Repository: microsoft/terminal (language: cpp)

Issue:
Support `EnableColorSelection` in Terminal
# Support EnableColorSelection feature in Terminal

The legacy conhost has a (somewhat hidden, I think) feature called "color selection", which allows you to select text and then use some keystrokes to change the fg/bg color of the selected text (including optional search). (for a description, see [here](https://tanelpoder.com/2007/09/01/sqlplus-is-my-second-home-part-3-colored-selections-in-windows-xp-command-prompt/))

This feature is not on by default, and must be enabled by setting a registry value.

I LOVE this feature and use it all the time (and when people see me use it, they all want to know how to turn it on). Hence I would love to port this feature to Terminal as well.

# Proposed technical implementation details

I think this would probably need to be disabled by default, and only enabled by a profile setting. Based on a [recent comment](https://github.com/microsoft/terminal/issues/8572#issuecomment-743955664) by Dustin, I believe he has an aversion to things that manipulate the buffer, but I don't know *why* that might be considered undesirable, or what other options might be--if you want this particular selection to be green, then you have to store that information somewhere--why *not* in the buffer; if not in the buffer, then where?

If the team is not opposed to having optional support for this feature, I'd be interested in taking a stab at it. I would appreciate any pointers to help me get a toe-hold in the code.



Discussion / hints:
For my future reference: `Selection::_HandleColorSelection`

I'm definitely of the same mind as Dustin in this case - I'm extremely reluctant to allow the user to modify the buffer contents like this. 
* If the region being changed is in the mutable viewport (at the "bottom" of the buffer), then we'd need to communicate these changes back to the underlying conpty as well. I'm not sure there's a VT sequence currently for "set the attributes in this region to this value". We could co-opt that as an input sequence, but it's definitely extremely hacky.
  * This of course wouldn't work if the thing on the other side wasn't a conpty. I guess in that case we wouldn't have to worry too much - if it's not a conpty, then the client app won't be reading the contents of the buffer anyways.
  * Though, if the terminal had like, a direct connection to a linux machine that was then ssh'ing to a Windows machine, then it's 
    `Terminal <--direct connection --> linux <-- ssh --> Windows sshd (using conpty)`
    and then the Terminal _is_ connected to a conpty, even if it doesn't know it.
* If the region being changed is in the scrollback, then I suppose we don't have to worry too much - that won't be in the conpty buffer, so we don't need to worry about it round-tripping

It might make more sense to not handle this in the conpty buffer itself, but maybe as something else entirely - like another layer of attributes applied on top of the ones in the buffer. We had discussed in the past something like having "overlay" buffers - maybe we could do something like that to "overlay" these color regions on the Terminal buffer contents. Then we wouldn't need to worry about conpty at all.

The original UX in conhost is also incredibly undiscoverable. I'd also think that if we implement this in the Terminal, we could come up with a better UI for this, not necessarily implement it exactly the same.
> * If the region being changed is in the mutable viewport (at the "bottom" of the buffer), then we'd need to communicate these changes back to the underlying conpty as well.

If we ever get around to reimplementing conpty so it's purely passing through the VT sequences, then it would never need to "redraw" the mutable viewport, so we wouldn't need to communicate the changes back to the conhost side. I think that's the only way a feature like this would be practical (unless of course you went with a completely different approach - like that overlay idea).

> * I'm not sure there's a VT sequence currently for "set the attributes in this region to this value". We could co-opt that as an input sequence, but it's definitely extremely hacky.

There is such a sequence ([`DECCARA`](https://vt100.net/docs/vt510-rm/DECCARA.html)), but I wouldn't think it appropriate to use as an input sequence.

For the record, I'm neither for or against this feature. I've used it a few times as a party-trick to amaze friends and family, but it's not something I really need.
It *is* a good party trick, right? :D

> then we'd need to communicate these changes back to the underlying conpty as well. 

For sake of discussion (and my education): what if we didn't?

I'm guessing the "bad thing" that could happen is an app asks about buffer contents at some (now-modified by the user) location, Terminal gives back the [modified] content, and the app says "what the????". Is that the problem/concern? (Or is it something else?)

My mental model of this feature is that it really is a UI feature "on top", so maybe the "overlay" idea really is the way to go. On the other hand, if we had an "export the entire buffer" feature (to file, or HTML, or whatever), I think I would want the user's modifications to be included... so it seems like actually modifying the buffer would be most straightforward for that.

Maybe most apps just don't really need to know that the user is coloring on their buffer? And if you have some apps that ARE sensitive to this, you just don't enable this feature? (or use it when using these apps)

Re: discoverability: I don't think there is anything so much worse about this feature than most. Just to figure out tabs and panes I had to read the documentation like for any other feature (and was frustrated and confused when the documentation was wrong, ha), so I don't see how EnableColorSelection is any different.


>  what if we didn't?

Conpty tries it's absolute best to not repaint things that haven't actually changed, but it isn't always the best at that. If you only change the terminal buffer and not the underlying conpty one, then it's absolutely possible that conpty might decide to repaint a section that had the attributes changed, and blow away the modifications that only reside in the terminal. Hence why we might want to go with the overlay route - just leave the underlying buffer out of the question.  
It just occurred to me that the overlay concept doesn't really solve this problem. Imagine you've got some VT margins set, and you highlight a piece of text somewhere within the margin area, then you also scroll that area. Conpty is just going to redraw that area of the screen, but with all the content moved up one line. There's no way for the terminal to know that a scroll operation occurred, so it'll either draw the overlay in the wrong place, or maybe just erase it. Either way it's not what you want.
Hack project number 6:
![fhl-colorSelection-000](https://user-images.githubusercontent.com/18356694/160012624-9b544fe8-651c-480d-9e29-162522b3e019.gif)

Relevant diff:

https://github.com/microsoft/terminal/compare/d364eb4d4ee70545598a4d27d7667d20209de534...dev/migrie/fhl/9583-colorSelection

I'm probably gonna leave it at this for now, and tag it up as help wanted. This is definitely the "just change the Terminal buffer contents and don't worry about conpty" version. Works.... well enough. 

* [ ] I had a mind to make `SelectionColor` accept either a index in the color table or a RGB color, but only got so far as a color. Hence the weird `uint32_t` in there
* [ ] I didn't really validate the string parsing
* [ ] I wanted FG/BG to be optional/nullable, but I don't think that's gonna be possible, a `TextAttribute` needs both. So we should validate they're both not null, or maybe just use the Default FG/BG when omitted.
* [ ] Didn't bother with formatting the action name at all. Clearly, it's just got random garbage numbers in it. 

There's also a bunch of other stuff that's in `Selection::_HandleColorSelection` that I didn't do yet. 


If someone rounded out the corners, we'd probably take this as it is, as the `experimental.colorSelection` action, without having to worry about all the overlays, etc. Especially considering the scroll margins issue. 
> I had a mind to make SelectionColor accept either a index in the color table or a RGB color

Yes, I think this is important, because if I select some text and type <kbd>alt</kbd>+<kbd>6</kbd>, then I expect the text to become red, and not just *any* red, but the same red as is used by my current color theme.

I think this would be doable by sneaking a flag in the alpha channel (because we don't have access to TextColor or such things from the settings layer). And furthermore, I think it could be done in a way that basically only affects "color selection". And indexed colors would use a different syntax: so you'd say "#RRGGBB" for RGB, or "iNN" for an index.

Like so: see https://github.com/microsoft/terminal/compare/dev/migrie/fhl/9583-colorSelection...jazzdelightsme:user/danthom/ecs_demo_indexedColors

This allows perfect reproduction of the ctrl/alt combos from legacy conhost, with keybindings like:

```json
    {
        "keys": "alt+1",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i07", "background": "i00" }
    },
    {
        "keys": "alt+2",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i08", "background": "i00" }
    },
    {
        "keys": "alt+3",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i09", "background": "i00" }
    },
    {
        "keys": "alt+4",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i0a", "background": "i00" }
    },
    {
        "keys": "alt+5",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i0b", "background": "i00" }
    },
    {
        "keys": "alt+6",
        "command": { "action": "experimental.colorSelection",
                     "foreground": "i0c", "background": "i00" }
    },
    // etc.
    // background
    {
        "keys": "ctrl+1",
        "command": { "action": "experimental.colorSelection",
                     "background": "i07", "foreground": "i00" }
    },
    {
        "keys": "ctrl+2",
        "command": { "action": "experimental.colorSelection",
                     "background": "i08", "foreground": "i00" }
    },
    // etc.
```

@zadjii-msft: any objection to this approach?


Fix patch:
diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt
index 6717224e675..e7b0749498c 100644
--- a/.github/actions/spelling/allow/apis.txt
+++ b/.github/actions/spelling/allow/apis.txt
@@ -92,6 +92,7 @@ istream
 IStringable
 ITab
 ITaskbar
+itow
 IUri
 IVirtual
 KEYSELECT
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 311ad53a79a..c06780f669e 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -14,6 +14,12 @@
       "type": "string",
       "format": "color"
     },
+    "ColorOrIndex": {
+      "default": "#",
+      "pattern": "^(?:#[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{3})?|i[A-Fa-f0-9]{2})$",
+      "type": "string",
+      "format": "color"
+    },
     "Coordinates": {
       "pattern": "^(-?\\d+)?(,\\s?(-?\\d+)?)?$",
       "type": "string"
@@ -58,20 +64,20 @@
       "default": "",
       "description": "Sets the file location of the sound played when the application emits a BEL character. If the path is invalid no sound will be played. This property also accepts an array of sounds and the terminal will pick one at random.",
       "oneOf": [
-      {
-        "type": [
-          "string",
-          "null"
-        ]
-      },
-      {
-        "type": "array",
-        "items": {
-          "type": "string"
+        {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
         }
-      }
       ]
-    }, 
+    },
     "AppearanceConfig": {
       "properties": {
         "colorScheme": {
@@ -378,6 +384,7 @@
         "scrollToMark",
         "clearMark",
         "clearAllMarks",
+        "experimental.colorSelection",
         "unbound"
       ],
       "type": "string"
@@ -838,6 +845,43 @@
         }
       ]
     },
+    "ColorSelectionAction": {
+      "description": "Arguments corresponding to a Color Selection Action",
+      "allOf": [
+        {
+          "$ref": "#/$defs/ShortcutAction"
+        },
+        {
+          "properties": {
+            "action": {
+              "type": "string",
+              "const": "experimental.colorSelection"
+            },
+            "matchMode": {
+              "type": "string",
+              "default": "none",
+              "description": "Specifies if only the selected text should be colored (0), or all instances of selected text (case-insensitive) (1).",
+              "enum": [
+                "none",
+                "all"
+              ]
+            },
+            "foreground": {
+              "$ref": "#/$defs/ColorOrIndex",
+              "type": "string",
+              "default": "",
+              "description": "The foreground color to use, as an RGB value (\"#rrggbb\"), or color index (\"iNN\"). If left unspecified it falls back to the default text foreground color."
+            },
+            "background": {
+              "$ref": "#/$defs/ColorOrIndex",
+              "type": "string",
+              "default": "",
+              "description": "The background color to use, as an RGB value (\"#rrggbb\"), or color index (\"iNN\"). If left unspecified it falls back to the default text background color."
+            }
+          }
+        }
+      ]
+    },
     "OpenSettingsAction": {
       "description": "Arguments corresponding to a Open Settings Action",
       "allOf": [
@@ -1613,6 +1657,9 @@
             {
               "$ref": "#/$defs/AdjustOpacityAction"
             },
+            {
+              "$ref": "#/$defs/ColorSelectionAction"
+            },
             {
               "type": "null"
             }
@@ -1735,6 +1782,11 @@
           "description": "When set to true, URLs will be detected by the Terminal. This will cause URLs to underline on hover and be clickable by pressing Ctrl.",
           "type": "boolean"
         },
+        "experimental.enableColorSelection": {
+          "default": false,
+          "description": "When set to true, adds preset \"Color Selection\" actions (keybindings) to allow colorizing selected text via keystroke, similar to the legacy conhost EnableColorSelection feature (such as alt+6 to color the selection red).",
+          "type": "boolean"
+        },
         "disableAnimations": {
           "default": false,
           "description": "When set to `true`, visual animations will be disabled across the application.",
@@ -1974,7 +2026,7 @@
             "afterLastTab",
             "afterCurrentTab"
           ],
-          "type": "string"        
+          "type": "string"
         },
         "autoHideWindow": {
           "default": false,
diff --git a/src/buffer/out/LineRendition.hpp b/src/buffer/out/LineRendition.hpp
index db7a5489ca7..7be4a7b59bf 100644
--- a/src/buffer/out/LineRendition.hpp
+++ b/src/buffer/out/LineRendition.hpp
@@ -28,6 +28,13 @@ constexpr til::inclusive_rect ScreenToBufferLine(const til::inclusive_rect& line
     return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom };
 }
 
+constexpr til::point ScreenToBufferLine(const til::point& line, const LineRendition lineRendition)
+{
+    // Use shift right to quickly divide the Left and Right by 2 for double width lines.
+    const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1;
+    return { line.X >> scale, line.Y };
+}
+
 constexpr til::inclusive_rect BufferToScreenLine(const til::inclusive_rect& line, const LineRendition lineRendition)
 {
     // Use shift left to quickly multiply the Left and Right by 2 for double width lines.
diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp
index 30731eec7cc..6bbcdc1ae68 100644
--- a/src/buffer/out/search.cpp
+++ b/src/buffer/out/search.cpp
@@ -23,7 +23,7 @@ using namespace Microsoft::Console::Types;
 // - direction - The direction to search (upward or downward)
 // - sensitivity - Whether or not you care about case
 Search::Search(IUiaData& uiaData,
-               const std::wstring& str,
+               const std::wstring_view str,
                const Direction direction,
                const Sensitivity sensitivity) :
     _direction(direction),
@@ -47,7 +47,7 @@ Search::Search(IUiaData& uiaData,
 // - sensitivity - Whether or not you care about case
 // - anchor - starting search location in screenInfo
 Search::Search(IUiaData& uiaData,
-               const std::wstring& str,
+               const std::wstring_view str,
                const Direction direction,
                const Sensitivity sensitivity,
                const til::point anchor) :
@@ -106,14 +106,13 @@ void Search::Select() const
 }
 
 // Routine Description:
-// - In console host, we take the found word and apply the given color to it in the screen buffer
-// - In Windows Terminal, we just select the found word, but we do not modify the buffer
+// - Applies the supplied TextAttribute to the current search result.
 // Arguments:
-// - ulAttr - The legacy color attribute to apply to the word
+// - attr - The attribute to apply to the result
 void Search::Color(const TextAttribute attr) const
 {
     // Only select if we've found something.
-    if (_coordSelStart != _coordSelEnd)
+    if (_coordSelEnd >= _coordSelStart)
     {
         _uiaData.ColorSelection(_coordSelStart, _coordSelEnd, attr);
     }
@@ -330,7 +329,7 @@ void Search::_UpdateNextPosition()
 // - wstr - String that will be our search term
 // Return Value:
 // - Structured text data for comparison to screen buffer text data.
-std::vector<std::vector<wchar_t>> Search::s_CreateNeedleFromString(const std::wstring& wstr)
+std::vector<std::vector<wchar_t>> Search::s_CreateNeedleFromString(const std::wstring_view wstr)
 {
     const auto charData = Utf16Parser::Parse(wstr);
     std::vector<std::vector<wchar_t>> cells;
diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h
index 7f30245ec5b..c5581f2bf8b 100644
--- a/src/buffer/out/search.h
+++ b/src/buffer/out/search.h
@@ -41,12 +41,12 @@ class Search final
     };
 
     Search(Microsoft::Console::Types::IUiaData& uiaData,
-           const std::wstring& str,
+           const std::wstring_view str,
            const Direction dir,
            const Sensitivity sensitivity);
 
     Search(Microsoft::Console::Types::IUiaData& uiaData,
-           const std::wstring& str,
+           const std::wstring_view str,
            const Direction dir,
            const Sensitivity sensitivity,
            const til::point anchor);
@@ -68,7 +68,7 @@ class Search final
 
     static til::point s_GetInitialAnchor(const Microsoft::Console::Types::IUiaData& uiaData, const Direction dir);
 
-    static std::vector<std::vector<wchar_t>> s_CreateNeedleFromString(const std::wstring& wstr);
+    static std::vector<std::vector<wchar_t>> s_CreateNeedleFromString(const std::wstring_view wstr);
 
     bool _reachedEnd = false;
     til::point _coordNext;
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index ac699ec4351..2c7e0359387 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -1611,7 +1611,7 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos, std::optional<til::point>
 // - bufferCoordinates: when enabled, treat the coordinates as relative to
 //                      the buffer rather than the screen.
 // Return Value:
-// - the delimiter class for the given char
+// - One or more rects corresponding to the selection area
 const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
 {
     std::vector<til::inclusive_rect> textRects;
@@ -1660,6 +1660,69 @@ const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start
     return textRects;
 }
 
+// Method Description:
+// - Computes the span(s) for the given selection
+// - If not a blockSelection, returns a single span (start - end)
+// - Else if a blockSelection, returns spans corresponding to each line in the block selection
+// Arguments:
+// - start: beginning of the text region of interest (inclusive)
+// - end: the other end of the text region of interest (inclusive)
+// - blockSelection: when enabled, get spans for each line covered by the block
+// - bufferCoordinates: when enabled, treat the coordinates as relative to
+//                      the buffer rather than the screen.
+// Return Value:
+// - one or more sets of start-end coordinates, representing spans of text in the buffer
+std::vector<til::point_span> TextBuffer::GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
+{
+    std::vector<til::point_span> textSpans;
+    if (blockSelection)
+    {
+        // If blockSelection, this is effectively the same operation as GetTextRects, but
+        // expressed in til::point coordinates.
+        const auto rects = GetTextRects(start, end, /*blockSelection*/ true, bufferCoordinates);
+        textSpans.reserve(rects.size());
+
+        for (auto rect : rects)
+        {
+            const til::point first = { rect.Left, rect.Top };
+            const til::point second = { rect.Right, rect.Bottom };
+            textSpans.emplace_back(first, second);
+        }
+    }
+    else
+    {
+        const auto bufferSize = GetSize();
+
+        // (0,0) is the top-left of the screen
+        // the physically "higher" coordinate is closer to the top-left
+        // the physically "lower" coordinate is closer to the bottom-right
+        auto [higherCoord, lowerCoord] = start <= end ?
+                                             std::make_tuple(start, end) :
+                                             std::make_tuple(end, start);
+
+        textSpans.reserve(1);
+
+        // If we were passed screen coordinates, convert the given range into
+        // equivalent buffer offsets, taking line rendition into account.
+        if (!bufferCoordinates)
+        {
+            higherCoord = ScreenToBufferLine(higherCoord, GetLineRendition(higherCoord.Y));
+            lowerCoord = ScreenToBufferLine(lowerCoord, GetLineRendition(lowerCoord.Y));
+        }
+
+        til::inclusive_rect asRect = { higherCoord.X, higherCoord.Y, lowerCoord.X, lowerCoord.Y };
+        _ExpandTextRow(asRect);
+        higherCoord.X = asRect.Left;
+        higherCoord.Y = asRect.Top;
+        lowerCoord.X = asRect.Right;
+        lowerCoord.Y = asRect.Bottom;
+
+        textSpans.emplace_back(higherCoord, lowerCoord);
+    }
+
+    return textSpans;
+}
+
 // Method Description:
 // - Expand the selection row according to include wide glyphs fully
 // - this is particularly useful for box selections (ALT + selection)
@@ -1774,9 +1837,8 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
                     }
                 }
             }
-#pragma warning(suppress : 26444)
-            // TODO GH 2675: figure out why there's custom construction/destruction happening here
-            it++;
+
+            ++it;
         }
 
         // We apply formatting to rows if the row was NOT wrapped or formatting of wrapped rows is allowed
@@ -1832,6 +1894,43 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
     return data;
 }
 
+size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
+{
+    const auto bufferSize = GetSize();
+    // The coords are inclusive, so to get the (inclusive) length we add 1.
+    const auto length = bufferSize.CompareInBounds(coordEnd, coordStart) + 1;
+    return gsl::narrow<size_t>(length);
+}
+
+// Routine Description:
+// - Retrieves the plain text data between the specified coordinates.
+// Arguments:
+// - trimTrailingWhitespace - remove the trailing whitespace at the end of the result.
+// - start - where to start getting text (should be at or prior to "end")
+// - end - where to end getting text
+// Return Value:
+// - Just the text.
+std::wstring TextBuffer::GetPlainText(const til::point& start, const til::point& end) const
+{
+    std::wstring text;
+    auto spanLength = SpanLength(start, end);
+    text.reserve(spanLength);
+
+    auto it = GetCellDataAt(start);
+
+    for (; it && spanLength > 0; ++it, --spanLength)
+    {
+        const auto& cell = *it;
+        if (!cell.DbcsAttr().IsTrailing())
+        {
+            const auto chars = cell.Chars();
+            text.append(chars);
+        }
+    }
+
+    return text;
+}
+
 // Routine Description:
 // - Generates a CF_HTML compliant structure based on the passed in text and color data
 // Arguments:
diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp
index 61f82599acd..17cfb21b6da 100644
--- a/src/buffer/out/textBuffer.hpp
+++ b/src/buffer/out/textBuffer.hpp
@@ -166,6 +166,7 @@ class TextBuffer final
     bool MoveToPreviousGlyph(til::point& pos, std::optional<til::point> limitOptional = std::nullopt) const;
 
     const std::vector<til::inclusive_rect> GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const;
+    std::vector<til::point_span> GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const;
 
     void AddHyperlinkToMap(std::wstring_view uri, uint16_t id);
     std::wstring GetHyperlinkUriFromId(uint16_t id) const;
@@ -182,12 +183,16 @@ class TextBuffer final
         std::vector<std::vector<COLORREF>> BkAttr;
     };
 
+    size_t SpanLength(const til::point coordStart, const til::point coordEnd) const;
+
     const TextAndColor GetText(const bool includeCRLF,
                                const bool trimTrailingWhitespace,
                                const std::vector<til::inclusive_rect>& textRects,
                                std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors = nullptr,
                                const bool formatWrappedRows = false) const;
 
+    std::wstring GetPlainText(const til::point& start, const til::point& end) const;
+
     static std::string GenHTML(const TextAndColor& rows,
                                const int fontHeightPoints,
                                const std::wstring_view fontFaceName,
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 23960046f49..18cae0d5262 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1157,6 +1157,21 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/,
+                                             const ActionEventArgs& args)
+    {
+        if (args)
+        {
+            if (const auto& realArgs = args.ActionArgs().try_as<ColorSelectionArgs>())
+            {
+                const auto res = _ApplyToActiveControls([&](auto& control) {
+                    control.ColorSelection(realArgs.Foreground(), realArgs.Background(), realArgs.MatchMode());
+                });
+                args.Handled(res);
+            }
+        }
+    }
+
     void TerminalPage::_HandleExpandSelectionToWord(const IInspectable& /*sender*/,
                                                     const ActionEventArgs& args)
     {
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index fdf5d082fc8..ef2f88e60b5 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -17,6 +17,7 @@
 #include "../../renderer/dx/DxRenderer.hpp"
 
 #include "ControlCore.g.cpp"
+#include "SelectionColor.g.cpp"
 
 using namespace ::Microsoft::Console::Types;
 using namespace ::Microsoft::Console::VirtualTerminal;
@@ -78,6 +79,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return initialized;
     }
 
+    TextColor SelectionColor::AsTextColor() const noexcept
+    {
+        if (_IsIndex16)
+        {
+            return { _Color.r, false };
+        }
+        else
+        {
+            return { static_cast<COLORREF>(_Color) };
+        }
+    }
+
     ControlCore::ControlCore(Control::IControlSettings settings,
                              Control::IControlAppearance unfocusedAppearance,
                              TerminalConnection::ITerminalConnection connection) :
@@ -2164,4 +2177,40 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             }
         }
     }
+
+    void ControlCore::ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode)
+    {
+        if (HasSelection())
+        {
+            const auto pForeground = winrt::get_self<implementation::SelectionColor>(fg);
+            const auto pBackground = winrt::get_self<implementation::SelectionColor>(bg);
+
+            TextColor foregroundAsTextColor;
+            TextColor backgroundAsTextColor;
+
+            if (pForeground)
+            {
+                foregroundAsTextColor = pForeground->AsTextColor();
+            }
+
+            if (pBackground)
+            {
+                backgroundAsTextColor = pBackground->AsTextColor();
+            }
+
+            TextAttribute attr;
+            attr.SetForeground(foregroundAsTextColor);
+            attr.SetBackground(backgroundAsTextColor);
+
+            _terminal->ColorSelection(attr, matchMode);
+            _terminal->ClearSelection();
+            if (matchMode != Core::MatchMode::None)
+            {
+                // ClearSelection will invalidate the selection area... but if we are
+                // coloring other matches, then we need to make sure those get redrawn,
+                // too.
+                _renderer->TriggerRedrawAll();
+            }
+        }
+    }
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index f9a6297bd57..1f9463ae1e4 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -16,11 +16,13 @@
 #pragma once
 
 #include "ControlCore.g.h"
+#include "SelectionColor.g.h"
 #include "ControlSettings.h"
 #include "../../audio/midi/MidiAudio.hpp"
 #include "../../renderer/base/Renderer.hpp"
 #include "../../cascadia/TerminalCore/Terminal.hpp"
 #include "../buffer/out/search.h"
+#include "../buffer/out/TextColor.h"
 
 #include <til/ticket_lock.h>
 
@@ -40,6 +42,14 @@ public:                                                           \
 
 namespace winrt::Microsoft::Terminal::Control::implementation
 {
+    struct SelectionColor : SelectionColorT<SelectionColor>
+    {
+        TextColor AsTextColor() const noexcept;
+
+        WINRT_PROPERTY(til::color, Color);
+        WINRT_PROPERTY(bool, IsIndex16);
+    };
+
     struct ControlCore : ControlCoreT<ControlCore>
     {
     public:
@@ -106,6 +116,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         ::Microsoft::Console::Types::IUiaData* GetUiaData() const;
 
+        void ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode);
+
         void Close();
 
 #pragma region ICoreState
@@ -341,4 +353,5 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 namespace winrt::Microsoft::Terminal::Control::factory_implementation
 {
     BASIC_FACTORY(ControlCore);
+    BASIC_FACTORY(SelectionColor);
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index cb61cd2a3af..7ea4d7ee11b 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -49,6 +49,15 @@ namespace Microsoft.Terminal.Control
         Boolean EndAtRightBoundary;
     };
 
+    [default_interface] runtimeclass SelectionColor
+    {
+        SelectionColor();
+        Microsoft.Terminal.Core.Color Color;
+        // If true, color.R is a value between 0 and 15, indicating an indexed color.
+        // This mirrors how TextColor works internally, which is the primary target of this interface.
+        Boolean IsIndex16;
+    };
+
     [default_interface] runtimeclass ControlCore : ICoreState
     {
         ControlCore(IControlSettings settings,
@@ -130,6 +139,8 @@ namespace Microsoft.Terminal.Control
         void AdjustOpacity(Double Opacity, Boolean relative);
         void WindowVisibilityChanged(Boolean showOrHide);
 
+        void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode);
+
         event FontSizeChangedEventArgs FontSizeChanged;
 
         event Windows.Foundation.TypedEventHandler<Object, CopyToClipboardEventArgs> CopyToClipboard;
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index d8f25cace1d..db1306883c6 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -3114,4 +3114,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _core.ScrollMarks();
     }
 
+    void TermControl::ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, Core::MatchMode matchMode)
+    {
+        _core.ColorSelection(fg, bg, matchMode);
+    }
 }
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 34ac6dc3520..cfa0265adf9 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -49,6 +49,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         void WindowVisibilityChanged(const bool showOrHide);
 
+        void ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, Core::MatchMode matchMode);
+
 #pragma region ICoreState
         const uint64_t TaskbarState() const noexcept;
         const uint64_t TaskbarProgress() const noexcept;
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index e4b65735ab6..745e3d55664 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -93,5 +93,7 @@ namespace Microsoft.Terminal.Control
         Double BackgroundOpacity { get; };
 
         Windows.UI.Xaml.Media.Brush BackgroundBrush { get; };
+
+        void ColorSelection(SelectionColor fg, SelectionColor bg, Microsoft.Terminal.Core.MatchMode matchMode);
     }
 }
diff --git a/src/cascadia/TerminalCore/ICoreAppearance.idl b/src/cascadia/TerminalCore/ICoreAppearance.idl
index bf8abaf85d6..6d314969e9c 100644
--- a/src/cascadia/TerminalCore/ICoreAppearance.idl
+++ b/src/cascadia/TerminalCore/ICoreAppearance.idl
@@ -3,6 +3,12 @@
 
 namespace Microsoft.Terminal.Core
 {
+    enum MatchMode
+    {
+        None,
+        All
+    };
+
     enum CursorStyle
     {
         Vintage,
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 0648c6c0dc8..1cf4528a569 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -8,6 +8,7 @@
 #include "../../inc/unicode.hpp"
 #include "../../types/inc/utils.hpp"
 #include "../../types/inc/colorTable.hpp"
+#include "../../buffer/out/search.h"
 
 #include <winrt/Microsoft.Terminal.Core.h>
 
@@ -1668,6 +1669,41 @@ til::color Terminal::GetColorForMark(const Microsoft::Console::VirtualTerminal::
     }
 }
 
+void Terminal::ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode)
+{
+    for (const auto [start, end] : _GetSelectionSpans())
+    {
+        try
+        {
+            if (matchMode == winrt::Microsoft::Terminal::Core::MatchMode::None)
+            {
+                ColorSelection(start, end, attr);
+            }
+            else if (matchMode == winrt::Microsoft::Terminal::Core::MatchMode::All)
+            {
+                const auto textBuffer = _activeBuffer().GetPlainText(start, end);
+                std::wstring_view text{ textBuffer };
+
+                if (IsBlockSelection())
+                {
+                    text = Utils::TrimPaste(text);
+                }
+
+                if (!text.empty())
+                {
+                    Search search(*this, text, Search::Direction::Forward, Search::Sensitivity::CaseInsensitive, { 0, 0 });
+
+                    while (search.FindNext())
+                    {
+                        search.Color(attr);
+                    }
+                }
+            }
+        }
+        CATCH_LOG();
+    }
+}
+
 // Method Description:
 // - Returns the position of the cursor relative to the active viewport
 til::point Terminal::GetViewportRelativeCursorPosition() const noexcept
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 5608f51db4f..cc4ac0335d0 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -30,6 +30,7 @@ namespace winrt::Microsoft::Terminal::Core
     struct ICoreSettings;
     struct ICoreAppearance;
     struct Scheme;
+    enum class MatchMode;
 }
 
 namespace Microsoft::Console::VirtualTerminal
@@ -234,6 +235,8 @@ class Microsoft::Terminal::Core::Terminal final :
     const size_t GetTaskbarState() const noexcept;
     const size_t GetTaskbarProgress() const noexcept;
 
+    void ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode);
+
 #pragma region TextSelection
     // These methods are defined in TerminalSelection.cpp
     enum class SelectionInteractionMode
@@ -434,6 +437,7 @@ class Microsoft::Terminal::Core::Terminal final :
 #pragma region TextSelection
     // These methods are defined in TerminalSelection.cpp
     std::vector<til::inclusive_rect> _GetSelectionRects() const noexcept;
+    std::vector<til::point_span> _GetSelectionSpans() const noexcept;
     std::pair<til::point, til::point> _PivotSelection(const til::point targetPos, bool& targetStart) const;
     std::pair<til::point, til::point> _ExpandSelectionAnchors(std::pair<til::point, til::point> anchors) const;
     til::point _ConvertToBufferCell(const til::point viewportPos) const;
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index 38fa2446786..2dc0e2eadcd 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -63,6 +63,27 @@ std::vector<til::inclusive_rect> Terminal::_GetSelectionRects() const noexcept
     return result;
 }
 
+// Method Description:
+// - Identical to GetTextRects if it's a block selection, else returns a single span for the whole selection.
+// Return Value:
+// - A vector of one or more spans representing the selection. They are absolute coordinates relative to the buffer origin.
+std::vector<til::point_span> Terminal::_GetSelectionSpans() const noexcept
+{
+    std::vector<til::point_span> result;
+
+    if (!IsSelectionActive())
+    {
+        return result;
+    }
+
+    try
+    {
+        return _activeBuffer().GetTextSpans(_selection->start, _selection->end, _blockSelection, false);
+    }
+    CATCH_LOG();
+    return result;
+}
+
 // Method Description:
 // - Get the current anchor position relative to the whole text buffer
 // Arguments:
@@ -853,13 +874,14 @@ void Terminal::_ScrollToPoint(const til::point pos)
 }
 
 // Method Description:
-// - This method won't be used. We just throw and do nothing. For now we
-//   need this method to implement UiaData interface
+// - apply the TextAttribute "attr" to the active buffer
 // Arguments:
-// - coordSelectionStart - Not used
-// - coordSelectionEnd - Not used
-// - attr - Not used.
-void Terminal::ColorSelection(const til::point, const til::point, const TextAttribute)
+// - coordStart - where to begin applying attr
+// - coordEnd - where to end applying attr (inclusive)
+// - attr - the text attributes to apply
+void Terminal::ColorSelection(const til::point coordStart, const til::point coordEnd, const TextAttribute attr)
 {
-    THROW_HR(E_NOTIMPL);
+    size_t spanLength = _activeBuffer().SpanLength(coordStart, coordEnd);
+
+    _activeBuffer().Write(OutputCellIterator(attr, spanLength), coordStart);
 }
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 10dc2b4b31f..5f9f31386fd 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -84,6 +84,7 @@ static constexpr std::string_view SelectAllKey{ "selectAll" };
 static constexpr std::string_view MarkModeKey{ "markMode" };
 static constexpr std::string_view ToggleBlockSelectionKey{ "toggleBlockSelection" };
 static constexpr std::string_view SwitchSelectionEndpointKey{ "switchSelectionEndpoint" };
+static constexpr std::string_view ColorSelectionKey{ "experimental.colorSelection" };
 static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWord" };
 
 static constexpr std::string_view ActionKey{ "action" };
@@ -405,6 +406,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::MarkMode, RS_(L"MarkModeCommandKey") },
                 { ShortcutAction::ToggleBlockSelection, RS_(L"ToggleBlockSelectionCommandKey") },
                 { ShortcutAction::SwitchSelectionEndpoint, RS_(L"SwitchSelectionEndpointCommandKey") },
+                { ShortcutAction::ColorSelection, L"" }, // Intentionally omitted, must be generated by GenerateName
                 { ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") },
             };
         }();
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
index 3b3c56543a1..64c4976af19 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
@@ -43,6 +43,7 @@
 #include "ClearBufferArgs.g.cpp"
 #include "MultipleActionsArgs.g.cpp"
 #include "AdjustOpacityArgs.g.cpp"
+#include "ColorSelectionArgs.g.cpp"
 
 #include <LibraryResources.h>
 #include <WtExeUtils.h>
@@ -840,4 +841,119 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
             };
         }
     }
+
+    static winrt::hstring _FormatColorString(const Control::SelectionColor& selectionColor)
+    {
+        if (!selectionColor)
+        {
+            return RS_(L"ColorSelection_defaultColor");
+        }
+
+        const auto color = selectionColor.Color();
+        const auto isIndexed16 = selectionColor.IsIndex16();
+        winrt::hstring colorStr;
+
+        if (isIndexed16)
+        {
+            static const std::array indexedColorNames{
+                USES_RESOURCE(L"ColorSelection_Black"),
+                USES_RESOURCE(L"ColorSelection_Red"),
+                USES_RESOURCE(L"ColorSelection_Green"),
+                USES_RESOURCE(L"ColorSelection_Yellow"),
+                USES_RESOURCE(L"ColorSelection_Blue"),
+                USES_RESOURCE(L"ColorSelection_Purple"),
+                USES_RESOURCE(L"ColorSelection_Cyan"),
+                USES_RESOURCE(L"ColorSelection_White"),
+                USES_RESOURCE(L"ColorSelection_BrightBlack"),
+                USES_RESOURCE(L"ColorSelection_BrightRed"),
+                USES_RESOURCE(L"ColorSelection_BrightGreen"),
+                USES_RESOURCE(L"ColorSelection_BrightYellow"),
+                USES_RESOURCE(L"ColorSelection_BrightBlue"),
+                USES_RESOURCE(L"ColorSelection_BrightPurple"),
+                USES_RESOURCE(L"ColorSelection_BrightCyan"),
+                USES_RESOURCE(L"ColorSelection_BrightWhite"),
+            };
+            static_assert(indexedColorNames.size() == 16);
+
+            if (color.R < indexedColorNames.size())
+            {
+                colorStr = GetLibraryResourceString(til::at(indexedColorNames, color.R));
+            }
+            else
+            {
+                wchar_t tempBuf[9] = { 0 };
+                swprintf_s(tempBuf, L"i%02i", color.R);
+                colorStr = tempBuf;
+            }
+        }
+        else
+        {
+            colorStr = til::color{ color }.ToHexString(true);
+        }
+
+        return colorStr;
+    }
+
+    static bool _isBoringColor(const Control::SelectionColor& selectionColor)
+    {
+        if (!selectionColor)
+        {
+            return true;
+        }
+
+        const til::color color{ selectionColor.Color() };
+        return color.with_alpha(0) == til::color{};
+    }
+
+    winrt::hstring ColorSelectionArgs::GenerateName() const
+    {
+        auto matchModeStr = winrt::hstring{};
+        if (MatchMode() == Core::MatchMode::All)
+        {
+            matchModeStr = fmt::format(L", {}", RS_(L"ColorSelection_allMatches")); // ", all matches"
+        }
+
+        const auto foreground = Foreground();
+        const auto background = Background();
+        const auto fgStr = _FormatColorString(foreground);
+        const auto bgStr = _FormatColorString(background);
+
+        // To try to keep things simple for the user, we'll try to show only the
+        // "interesting" color (i.e. leave off the bg or fg if it is either unspecified or
+        // black or index 0).
+        //
+        // Note that we mask off the alpha channel, which is used to indicate if it's an
+        // indexed color.
+        const auto foregroundIsBoring = _isBoringColor(foreground);
+        const auto backgroundIsBoring = _isBoringColor(background);
+
+        if (foreground && backgroundIsBoring)
+        {
+            const auto str = RS_(L"ColorSelection_fg_action"); // "Color selection, foreground: {0}{1}"
+            return winrt::hstring{
+                fmt::format(std::wstring_view{ str }, fgStr, matchModeStr)
+            };
+        }
+        else if (background && foregroundIsBoring)
+        {
+            const auto str = RS_(L"ColorSelection_bg_action"); // "Color selection, background: {0}{1}"
+            return winrt::hstring{
+                fmt::format(std::wstring_view{ str }, bgStr, matchModeStr)
+            };
+        }
+        else if (foreground && background)
+        {
+            const auto str = RS_(L"ColorSelection_fg_bg_action"); // "Color selection, foreground: {0}, background: {1}{2}"
+            return winrt::hstring{
+                fmt::format(std::wstring_view{ str }, fgStr, bgStr, matchModeStr)
+            };
+        }
+        else
+        {
+            const auto str = RS_(L"ColorSelection_default_action"); // "Color selection, (default foreground/background){0}"
+            return winrt::hstring{
+                fmt::format(std::wstring_view{ str }, matchModeStr)
+            };
+        }
+    }
 }
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h
index 03f69252561..7dc91ac32e6 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.h
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h
@@ -45,6 +45,7 @@
 #include "ClearBufferArgs.g.h"
 #include "MultipleActionsArgs.g.h"
 #include "AdjustOpacityArgs.g.h"
+#include "ColorSelectionArgs.g.h"
 
 #include "JsonUtils.h"
 #include "HashUtils.h"
@@ -235,6 +236,12 @@ private:
     X(int32_t, Opacity, "opacity", false, 0) \
     X(bool, Relative, "relative", false, true)
 
+////////////////////////////////////////////////////////////////////////////////
+#define COLOR_SELECTION_ARGS(X)                                                                      \
+    X(winrt::Microsoft::Terminal::Control::SelectionColor, Foreground, "foreground", false, nullptr) \
+    X(winrt::Microsoft::Terminal::Control::SelectionColor, Background, "background", false, nullptr) \
+    X(winrt::Microsoft::Terminal::Core::MatchMode, MatchMode, "matchMode", false, winrt::Microsoft::Terminal::Core::MatchMode::None)
+
 ////////////////////////////////////////////////////////////////////////////////
 
 namespace winrt::Microsoft::Terminal::Settings::Model::implementation
@@ -384,6 +391,20 @@ struct til::hash_trait<winrt::Microsoft::Terminal::Settings::Model::NewTerminalA
         }
     }
 };
+template<>
+struct til::hash_trait<winrt::Microsoft::Terminal::Control::SelectionColor>
+{
+    using M = winrt::Microsoft::Terminal::Control::SelectionColor;
+
+    void operator()(hasher& h, const M& value) const noexcept
+    {
+        if (value)
+        {
+            h.write(value.Color());
+            h.write(value.IsIndex16());
+        }
+    }
+};
 
 namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 {
@@ -713,6 +734,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
     ACTION_ARGS_STRUCT(AdjustOpacityArgs, ADJUST_OPACITY_ARGS);
 
+    ACTION_ARGS_STRUCT(ColorSelectionArgs, COLOR_SELECTION_ARGS);
+
 }
 
 namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
index 77c817315b6..dba7587a69d 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
@@ -377,4 +377,11 @@ namespace Microsoft.Terminal.Settings.Model
         Int32 Opacity { get; };
         Boolean Relative { get; };
     };
+
+    [default_interface] runtimeclass ColorSelectionArgs : IActionArgs
+    {
+        Microsoft.Terminal.Control.SelectionColor Foreground;
+        Microsoft.Terminal.Control.SelectionColor Background;
+        Microsoft.Terminal.Core.MatchMode MatchMode { get; };
+    };
 }
diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp
index 9e7bb650ddf..a3a5124a3d0 100644
--- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp
@@ -196,7 +196,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
     {
         // Update NameMap with our parents.
         // Starting with this means we're doing a top-down approach.
-        assert(_parents.size() <= 1);
         for (const auto& parent : _parents)
         {
             parent->_PopulateNameMapWithSpecialCommands(nameMap);
@@ -274,7 +273,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         });
 
         // Now, add the accumulated actions from our parents
-        assert(_parents.size() <= 1);
         for (const auto& parent : _parents)
         {
             const auto parentActions{ parent->_GetCumulativeActions() };
@@ -367,7 +365,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         }
 
         // Update keyBindingsMap and unboundKeys with our parents
-        assert(_parents.size() <= 1);
         for (const auto& parent : _parents)
         {
             parent->_PopulateKeyBindingMapWithStandardCommands(keyBindingsMap, unboundKeys);
@@ -408,7 +405,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
             actionMap->_IterableCommands.emplace_back(*winrt::get_self<Command>(cmd)->Copy());
         }
 
-        assert(_parents.size() <= 1);
         actionMap->_parents.reserve(_parents.size());
         for (const auto& parent : _parents)
         {
@@ -518,7 +514,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         if (maskingActionPair == _MaskingActions.end())
         {
             // Check if we need to add this to our list of masking commands.
-            FAIL_FAST_IF(_parents.size() > 1);
             for (const auto& parent : _parents)
             {
                 // NOTE: This only checks the layer above us, but that's ok.
@@ -737,7 +732,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
         // the command was not bound in this layer,
         // ask my parents
-        assert(_parents.size() <= 1);
         for (const auto& parent : _parents)
         {
             const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) };
@@ -787,7 +781,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         }
 
         // Check our parents
-        assert(_parents.size() <= 1);
         for (const auto& parent : _parents)
         {
             if (const auto& keys{ parent->GetKeyBindingForAction(myAction, myArgs) })
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index 21c857971e8..efb861667c1 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -97,6 +97,7 @@
     ON_ALL_ACTIONS(MarkMode)                \
     ON_ALL_ACTIONS(ToggleBlockSelection)    \
     ON_ALL_ACTIONS(SwitchSelectionEndpoint) \
+    ON_ALL_ACTIONS(ColorSelection)          \
     ON_ALL_ACTIONS(ExpandSelectionToWord)   \
     ON_ALL_ACTIONS(CloseOtherPanes)
 
@@ -138,4 +139,5 @@
     ON_ALL_ACTIONS_WITH_ARGS(ExportBuffer)         \
     ON_ALL_ACTIONS_WITH_ARGS(ClearBuffer)          \
     ON_ALL_ACTIONS_WITH_ARGS(MultipleActions)      \
-    ON_ALL_ACTIONS_WITH_ARGS(AdjustOpacity)
+    ON_ALL_ACTIONS_WITH_ARGS(AdjustOpacity)        \
+    ON_ALL_ACTIONS_WITH_ARGS(ColorSelection)
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
index 5fd635e2465..cf1ea752cf1 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
@@ -22,6 +22,7 @@
 #include <LegacyProfileGeneratorNamespaces.h>
 
 #include "userDefaults.h"
+#include "enableColorSelection.h"
 
 #include "ApplicationState.h"
 #include "DefaultTerminal.h"
@@ -313,6 +314,16 @@ void SettingsLoader::FinalizeLayering()
 {
     // Layer default globals -> user globals
     userSettings.globals->AddLeastImportantParent(inboxSettings.globals);
+
+    // Actions are currently global, so if we want to conditionally light up a bunch of
+    // actions, this is the time to do it.
+    if (userSettings.globals->EnableColorSelection())
+    {
+        const auto json = _parseJson(EnableColorSelectionSettingsJson);
+        const auto globals = GlobalAppSettings::FromJson(json.root);
+        userSettings.globals->AddLeastImportantParent(globals);
+    }
+
     userSettings.globals->_FinalizeInheritance();
     // Layer default profile defaults -> user profile defaults
     userSettings.baseLayerProfile->AddLeastImportantParent(inboxSettings.baseLayerProfile);
diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp
index 14b5ac9b0a9..8b3d936a4a3 100644
--- a/src/cascadia/TerminalSettingsModel/EnumMappings.cpp
+++ b/src/cascadia/TerminalSettingsModel/EnumMappings.cpp
@@ -38,6 +38,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
     DEFINE_ENUM_MAP(Model::TabSwitcherMode, TabSwitcherMode);
     DEFINE_ENUM_MAP(Microsoft::Terminal::Control::CopyFormat, CopyFormat);
     DEFINE_ENUM_MAP(Model::WindowingMode, WindowingMode);
+    DEFINE_ENUM_MAP(Microsoft::Terminal::Core::MatchMode, MatchMode);
 
     // Profile Settings
     DEFINE_ENUM_MAP(Model::CloseOnExitMode, CloseOnExitMode);
diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.h b/src/cascadia/TerminalSettingsModel/EnumMappings.h
index fb89ee22257..dfdd6c46502 100644
--- a/src/cascadia/TerminalSettingsModel/EnumMappings.h
+++ b/src/cascadia/TerminalSettingsModel/EnumMappings.h
@@ -34,6 +34,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, TabSwitcherMode> TabSwitcherMode();
         static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::Microsoft::Terminal::Control::CopyFormat> CopyFormat();
         static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, WindowingMode> WindowingMode();
+        static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, winrt::Microsoft::Terminal::Core::MatchMode> MatchMode();
 
         // Profile Settings
         static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, CloseOnExitMode> CloseOnExitMode();
diff --git a/src/cascadia/TerminalSettingsModel/EnumMappings.idl b/src/cascadia/TerminalSettingsModel/EnumMappings.idl
index 6f704e2073a..a74dc96b483 100644
--- a/src/cascadia/TerminalSettingsModel/EnumMappings.idl
+++ b/src/cascadia/TerminalSettingsModel/EnumMappings.idl
@@ -16,6 +16,7 @@ namespace Microsoft.Terminal.Settings.Model
         static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Settings.Model.TabSwitcherMode> TabSwitcherMode { get; };
         static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Control.CopyFormat> CopyFormat { get; };
         static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Settings.Model.WindowingMode> WindowingMode { get; };
+        static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Core.MatchMode> MatchMode { get; };
 
         // Profile Settings
         static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Settings.Model.CloseOnExitMode> CloseOnExitMode { get; };
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
index 849cdd40824..a94ba185107 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
@@ -94,6 +94,7 @@ namespace Microsoft.Terminal.Settings.Model
         INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon);
         INHERITABLE_SETTING(IVector<String>, DisabledProfileSources);
         INHERITABLE_SETTING(Boolean, ShowAdminShield);
+        INHERITABLE_SETTING(Boolean, EnableColorSelection);
 
         Windows.Foundation.Collections.IMapView<String, ColorScheme> ColorSchemes();
         void AddColorScheme(ColorScheme scheme);
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 502b4adb404..b9dd7c8f98d 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -60,7 +60,8 @@ Author(s):
     X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false)                                                                               \
     X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr)                         \
     X(bool, ShowAdminShield, "showAdminShield", true)                                                                                                      \
-    X(bool, TrimPaste, "trimPaste", true)
+    X(bool, TrimPaste, "trimPaste", true)                                                                                                                  \
+    X(bool, EnableColorSelection, "experimental.enableColorSelection", false)
 
 #define MTSM_PROFILE_SETTINGS(X)                                                                                                                               \
     X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE)                                                                                               \
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
index fea18273d1d..04f0f38d7b8 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
@@ -295,6 +295,10 @@
   <Target Name="_TerminalAppGenerateUserSettingsH" Inputs="userDefaults.json" Outputs="Generated Files\userDefaults.h" BeforeTargets="BeforeClCompile">
     <Exec Command="pwsh.exe -NoProfile -ExecutionPolicy Unrestricted &quot;$(OpenConsoleDir)\tools\GenerateHeaderForJson.ps1&quot; -JsonFile userDefaults.json -OutPath &quot;Generated Files\userDefaults.h&quot; -VariableName UserSettingsJson" />
   </Target>
+  <!-- Same as above, but for the enableColorSelection actions -->
+  <Target Name="_TerminalAppGenerateEnableColorSelectionSettingsH" Inputs="enableColorSelection.json" Outputs="Generated Files\enableColorSelection.h" BeforeTargets="BeforeClCompile">
+    <Exec Command="pwsh.exe -NoProfile -ExecutionPolicy Unrestricted &quot;$(OpenConsoleDir)\tools\GenerateHeaderForJson.ps1&quot; -JsonFile enableColorSelection.json -OutPath &quot;Generated Files\enableColorSelection.h&quot; -VariableName EnableColorSelectionSettingsJson" />
+  </Target>
   <Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
 
 </Project>
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index 12fac66fa82..09998b9093f 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -567,6 +567,94 @@
   <data name="SwitchSelectionEndpointCommandKey" xml:space="preserve">
     <value>Switch selection endpoint</value>
   </data>
+  <data name="ColorSelection_allMatches" xml:space="preserve">
+    <value>all matches</value>
+    <comment>This is used in the description of an action which changes the color of selected text, to indicate that not only the selected text will be colored, but also all matching text.</comment>
+  </data>
+  <data name="ColorSelection_fg_action" xml:space="preserve">
+    <value>Color selection, foreground: {0}{1}</value>
+    <comment>This is the description of an action which changes the color of selected text. {0}: the color. {1}: empty string OR a clause indicating that all matching text will be colored (ColorSelection_allMatches).</comment>
+  </data>
+  <data name="ColorSelection_bg_action" xml:space="preserve">
+    <value>Color selection, background: {0}{1}</value>
+    <comment>This is the description of an action which changes the background color of selected text. {0}: the color. {1}: empty string OR a clause indicating that all matching text will be colored (ColorSelection_allMatches).</comment>
+  </data>
+  <data name="ColorSelection_fg_bg_action" xml:space="preserve">
+    <value>Color selection, foreground: {0}, background: {1}{2}</value>
+    <comment>This is the description of an action which changes the color of selected text and the background. {0}: the foreground color. {1}: the background color. {2}: empty string OR a clause indicating that all matching text will be colored (ColorSelection_allMatches).</comment>
+  </data>
+  <data name="ColorSelection_default_action" xml:space="preserve">
+    <value>Color selection, (default foreground/background){0}</value>
+    <comment>This is the description of an action which changes the color of selected text and the background to the default colors. {0}: empty string OR a clause indicating that all matching text will be colored (ColorSelection_allMatches).</comment>
+  </data>
+  <data name="ColorSelection_defaultColor" xml:space="preserve">
+    <value>[default]</value>
+    <comment>This is used in the description of an action which changes the color of selected text, as a placeholder for a color, to indicate that the default (foreground or background) color will be used.</comment>
+  </data>
+  <data name="ColorSelection_Black" xml:space="preserve">
+    <value>black</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Red" xml:space="preserve">
+    <value>red</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Green" xml:space="preserve">
+    <value>green</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Yellow" xml:space="preserve">
+    <value>yellow</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Blue" xml:space="preserve">
+    <value>blue</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Purple" xml:space="preserve">
+    <value>purple</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_Cyan" xml:space="preserve">
+    <value>cyan</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_White" xml:space="preserve">
+    <value>white</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightBlack" xml:space="preserve">
+    <value>bright black</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightRed" xml:space="preserve">
+    <value>bright red</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightGreen" xml:space="preserve">
+    <value>bright green</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightYellow" xml:space="preserve">
+    <value>bright yellow</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightBlue" xml:space="preserve">
+    <value>bright blue</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightPurple" xml:space="preserve">
+    <value>bright purple</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightCyan" xml:space="preserve">
+    <value>bright cyan</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
+  <data name="ColorSelection_BrightWhite" xml:space="preserve">
+    <value>bright white</value>
+    <comment>A color used in the "ColorSelection" action.</comment>
+  </data>
   <data name="ExpandSelectionToWordCommandKey" xml:space="preserve">
     <value>Expand selection to word</value>
   </data>
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
index c3f3f1e1f5e..a26c0b7e73d 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
@@ -76,6 +76,14 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollbarState)
     };
 };
 
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Core::MatchMode)
+{
+    static constexpr std::array<pair_type, 2> mappings = {
+        pair_type{ "none", ValueType::None },
+        pair_type{ "all", ValueType::All }
+    };
+};
+
 JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::BellStyle)
 {
     static constexpr std::array<pair_type, 6> mappings = {
@@ -679,3 +687,61 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
         pair_type{ "last", ValueType::Last },
     };
 };
+
+template<>
+struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winrt::Microsoft::Terminal::Control::SelectionColor>
+{
+    ::winrt::Microsoft::Terminal::Control::SelectionColor FromJson(const Json::Value& json)
+    {
+        const auto string = Detail::GetStringView(json);
+        const auto isIndexed16 = string.size() == 3 && string.front() == 'i';
+        til::color color;
+
+        if (isIndexed16)
+        {
+            const auto indexStr = string.substr(1);
+            const auto idx = til::to_ulong(indexStr, 16);
+            color.r = gsl::narrow_cast<uint8_t>(std::min(idx, 15ul));
+        }
+        else
+        {
+            color = ::Microsoft::Console::Utils::ColorFromHexString(string);
+        }
+
+        winrt::Microsoft::Terminal::Control::SelectionColor selection;
+        selection.Color(color);
+        selection.IsIndex16(isIndexed16);
+        return selection;
+    }
+
+    bool CanConvert(const Json::Value& json)
+    {
+        if (!json.isString())
+        {
+            return false;
+        }
+
+        const auto string = Detail::GetStringView(json);
+        const auto isColorSpec = (string.length() == 9 || string.length() == 7 || string.length() == 4) && string.front() == '#';
+        const auto isIndexedColor = string.size() == 3 && string.front() == 'i';
+        return isColorSpec || isIndexedColor;
+    }
+
+    Json::Value ToJson(const ::winrt::Microsoft::Terminal::Control::SelectionColor& val)
+    {
+        const auto color = val.Color();
+        if (val.IsIndex16())
+        {
+            return fmt::format("i{:02x}", color.R);
+        }
+        else
+        {
+            return ::Microsoft::Console::Utils::ColorToHexString(color);
+        }
+    }
+
+    std::string TypeDescription() const
+    {
+        return "SelectionColor (#rrggbb, #rgb, #rrggbbaa, iNN)";
+    }
+};
diff --git a/src/cascadia/TerminalSettingsModel/enableColorSelection.json b/src/cascadia/TerminalSettingsModel/enableColorSelection.json
new file mode 100644
index 00000000000..0df6d750786
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/enableColorSelection.json
@@ -0,0 +1,370 @@
+{
+    "actions":
+    [
+        // Foreground
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection"
+                // default fg and bg (i07 and i00)
+            },
+            "keys": "alt+1"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i08"
+            },
+            "keys": "alt+2"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0c"
+            },
+            "keys": "alt+3"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0a"
+            },
+            "keys": "alt+4"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0e"
+            },
+            "keys": "alt+5"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i09"
+            },
+            "keys": "alt+6"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0d"
+            },
+            "keys": "alt+7"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0b"
+            },
+            "keys": "alt+8"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i0f"
+            },
+            "keys": "alt+9"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i03"
+            },
+            "keys": "alt+0"
+        },
+        // background
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i07"
+            },
+            "keys": "ctrl+1"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i08"
+            },
+            "keys": "ctrl+2"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0c"
+            },
+            "keys": "ctrl+3"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0a"
+            },
+            "keys": "ctrl+4"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0e"
+            },
+            "keys": "ctrl+5"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i09"
+            },
+            "keys": "ctrl+6"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0d"
+            },
+            "keys": "ctrl+7"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0b"
+            },
+            "keys": "ctrl+8"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i0f"
+            },
+            "keys": "ctrl+9"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "foreground": "i00",
+                "background": "i03"
+            },
+            "keys": "ctrl+0"
+        },
+        // with matching
+        // Foreground, all matches
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                // default fg and bg (i07 and i00)
+            },
+            "keys": "alt+shift+1"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i08"
+            },
+            "keys": "alt+shift+2"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0c"
+            },
+            "keys": "alt+shift+3"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0a"
+            },
+            "keys": "alt+shift+4"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0e"
+            },
+            "keys": "alt+shift+5"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i09"
+            },
+            "keys": "alt+shift+6"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0d"
+            },
+            "keys": "alt+shift+7"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0b"
+            },
+            "keys": "alt+shift+8"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i0f"
+            },
+            "keys": "alt+shift+9"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i03"
+            },
+            "keys": "alt+shift+0"
+        },
+        // background, all matches
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i07"
+            },
+            "keys": "ctrl+shift+1"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i08"
+            },
+            "keys": "ctrl+shift+2"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0c"
+            },
+            "keys": "ctrl+shift+3"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0a"
+            },
+            "keys": "ctrl+shift+4"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0e"
+            },
+            "keys": "ctrl+shift+5"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i09"
+            },
+            "keys": "ctrl+shift+6"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0d"
+            },
+            "keys": "ctrl+shift+7"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0b"
+            },
+            "keys": "ctrl+shift+8"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i0f"
+            },
+            "keys": "ctrl+shift+9"
+        },
+        {
+            "command":
+            {
+                "action": "experimental.colorSelection",
+                "matchMode": "all",
+                "foreground": "i00",
+                "background": "i03"
+            },
+            "keys": "ctrl+shift+0"
+        }
+    ]
+}
diff --git a/src/inc/til/point.h b/src/inc/til/point.h
index 4177e448a0a..29d2a9e2f5a 100644
--- a/src/inc/til/point.h
+++ b/src/inc/til/point.h
@@ -270,6 +270,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
         }
         RETURN_WIN32(ERROR_UNHANDLED_EXCEPTION);
     }
+
+    // point_span can be pictured as a "selection" range inside our text buffer. So given
+    // a text buffer of 10x4, a start of 4,1 and end of 7,3 the span might look like this:
+    //   +----------+
+    //   |          |
+    //   |    xxxxxx|
+    //   |xxxxxxxxxx|
+    //   |xxxxxxxx  |
+    //   +----------+
+    // At the time of writing there's a push to make selections have an exclusive end coordinate,
+    // so the interpretation of end might change soon (making this comment potentially outdated).
+    struct point_span
+    {
+        til::point start;
+        til::point end;
+    };
 }
 
 #ifdef __WEX_COMMON_H__


Test patch: