Repository: dandavison/delta
Pull request #782 (2021-11-18 22:41:37)

Navigate regexp
In my navigating of delta diffs, I'd prefer to not stop at each hunk.  My first thought was to override the navigate_regexp value, but it wasn't configurable.  Then I had the idea to change the hunk-label to an empty string, which then made n/N stop at every line.

This branch makes the following changes:

* Turn --navigate-regexp into an option that defaults to an empty string.  When the string is empty, it is auto-populated with a value based on the various labels (the old behavior).
* Change the make_navigate_regexp() func to not add empty regex options to the string.
* Change the code that outputs the hunk-label to output it whenever it is non-empty, not when --navigate is specified. This (1) makes the option work all the time like other labels, and (2) ensures that an empty hunk-label doesn't add a leading space when --navigate is specified.

With these changes I've chosen to not override navigate-regexp and to just force hunk-label to an empty string in my git config. This avoids stopping at each hunk while also making it easy to manually specify an override --hunk-label option when I want to change my mind.  So, if you don't like the first commit in this branch (making navigate_regexp an option) the other 2 commits are good enough for my needs.

th1000s: Indeed, the `--navigate-regexp` should be user configurable.

So if I understand correctly, this can make navigation more fine grained by passing something that matches every line? Do you have ideas for other possible regexes which improve navigation?

I've also been playing around with this and want to add a nav marker every term-height/2 changed lines to still be able to jump around when using a lot of context such as `-U200`, but I have not found a good spot to place them (or hide via ANSI magic) yet.
WayneD: I'm wanting to avoid matching every line.  With the current code, an option of `--hunk-title ''` results in an empty option in the auto-gen regex (`'^(commit|added:|removed:|renamed:|Δ|)'`) which matches every line, making the skipping useless.  The new code avoids adding an empty label in such a case (`'^(commit|added:|removed:|renamed:|Δ)'`) as well as any other empty label (e.g. also specifying `--file-modified-label ''` would result in `'^(commit|added:|removed:|renamed:)'`).

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 0c081ac..042c6e4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -44,6 +44,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "approx"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "arrayvec"
 version = "0.5.2"
@@ -284,6 +293,15 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "find-crate"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
+dependencies = [
+ "toml",
+]
+
 [[package]]
 name = "flate2"
 version = "1.0.19"
@@ -354,8 +372,11 @@ dependencies = [
  "grep-cli",
  "itertools",
  "lazy_static",
+ "palette",
  "pathdiff",
  "regex",
+ "serde",
+ "serde_json",
  "shell-words",
  "smol_str",
  "structopt",
@@ -648,6 +669,30 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "palette"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9735f7e1e51a3f740bacd5dc2724b61a7806f23597a8736e679f38ee3435d18"
+dependencies = [
+ "approx",
+ "num-traits",
+ "palette_derive",
+ "phf",
+]
+
+[[package]]
+name = "palette_derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7799c3053ea8a6d8a1193c7ba42f534e7863cf52e378a7f90406f4a645d33bad"
+dependencies = [
+ "find-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "pathdiff"
 version = "0.2.1"
@@ -660,6 +705,50 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
+[[package]]
+name = "phf"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.19"
@@ -680,6 +769,12 @@ dependencies = [
  "xml-rs",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -704,6 +799,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.24"
@@ -722,6 +823,46 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.3",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.1.57"
@@ -806,6 +947,9 @@ name = "serde"
 version = "1.0.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
+dependencies = [
+ "serde_derive",
+]
 
 [[package]]
 name = "serde_derive"
@@ -820,9 +964,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.61"
+version = "1.0.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
+checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
 dependencies = [
  "itoa",
  "ryu",
@@ -835,11 +979,20 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
 
+[[package]]
+name = "siphasher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
+
 [[package]]
 name = "smol_str"
-version = "0.1.18"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b203e79e90905594272c1c97c7af701533d42adaab0beb3859018e477d54a3b0"
+checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "strsim"
@@ -973,6 +1126,15 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.4"
diff --git a/Cargo.toml b/Cargo.toml
index e7181dd..daaa3ed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,10 +29,13 @@ dirs-next = "2.0.0"
 grep-cli = "0.1.6"
 itertools = "0.10.1"
 lazy_static = "1.4"
+palette = "0.6.0"
 pathdiff = "0.2.1"
 regex = "1.4.6"
+serde = { version = "1.0.118", features = ["derive"] }
+serde_json = "1.0.70"
 shell-words = "1.0.0"
-smol_str = "0.1.18"
+smol_str = "0.1.21"
 structopt = "0.3.25"
 unicode-segmentation = "1.8.0"
 unicode-width = "0.1.9"
diff --git a/README.md b/README.md
index 8cc6b0a..1c4b870 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@
     show = delta
     log = delta
     blame = delta
+    grep = delta
     reflog = delta
 
 [interactive]
@@ -64,6 +65,7 @@ Code evolves, and we all spend time studying diffs. Delta aims to make this both
   - [Choosing colors (styles)](#choosing-colors-styles)
   - [Line numbers](#line-numbers)
   - [Side-by-side view](#side-by-side-view)
+  - [Grep](#grep)
   - ["Features": named groups of settings](#features-named-groups-of-settings)
   - [Custom themes](#custom-themes)
   - [diff-highlight and diff-so-fancy emulation](#diff-highlight-and-diff-so-fancy-emulation)
@@ -151,6 +153,7 @@ Here's what `git show` can look like with git configured to use delta:
 - Git style strings (foreground color, background color, font attributes) are supported for >20 stylable elements
 - Side-by-side view with line-wrapping
 - Line numbering
+- Handles grep output with file paths from `rg`, `git grep`, `grep`, etc
 - `diff-highlight` and `diff-so-fancy` emulation modes
 - Stylable box/line decorations to draw attention to commit, file and hunk header sections.
 - Support for Git's `--color-moved` feature.
@@ -410,6 +413,15 @@ In contrast, the long replacement line in the right panel overflows by almost an
 For control over the details of line wrapping, see `--wrap-max-lines`, `--wrap-left-symbol`, `--wrap-right-symbol`, `--wrap-right-percent`, `--wrap-right-prefix-symbol`, `--inline-hint-style`.
 Line wrapping was implemented by @th1000s.
 
+### Grep
+
+Delta applies syntax-highlighting and other enhancements to standard grep output such as from `git grep`, [ripgrep](https://github.com/BurntSushi/ripgrep/) (aka `rg`), grep, etc.
+To use with `git grep`, set delta as the pager for `grep` in the `[pager]` section of your gitconfig. See the example at the [top of the page](#get-started).
+Output from other grep tools can be piped to delta: e.g. `rg -Hn --color=always`, `grep -Hn --color=always`, etc.
+To customize the colors and syntax highlighting, see `grep-match-line-style`, `grep-match-word-style`, `grep-contexct-line-style`, `grep-file-style`, `grep-line-number-style`.
+Ripgrep's `rg --json` output format is supported; this avoids certain file name parsing ambiguities that are inevitable with the standard grep output formats.
+Note that `git grep` can display the "function context" for matches and that delta handles this output specially: see the `-p` and `-W` options of `git grep`.
+
 ### "Features": named groups of settings
 
 All delta options can go under the `[delta]` section in your git config file. However, you can also use named "features" to keep things organized: these are sections in git config like `[delta "my-feature"]`. Here's an example using two custom features:
@@ -848,6 +860,9 @@ OPTIONS:
         --wrap-right-prefix-symbol <wrap-right-prefix-symbol>
             Symbol displayed in front of right-aligned wrapped content [default: …]
 
+        --navigate-regex <navigate-regex>
+            A regexp to use in the less pager when navigating (auto-generated when unspecified)
+
         --file-modified-label <file-modified-label>
             Text to display in front of a modified file path [default: ]
 
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs
index 643b47f..f45c13e 100644
--- a/src/ansi/mod.rs
+++ b/src/ansi/mod.rs
@@ -3,6 +3,7 @@ mod iterator;
 
 use std::borrow::Cow;
 
+use ansi_term::Style;
 use itertools::Itertools;
 use unicode_segmentation::UnicodeSegmentation;
 use unicode_width::UnicodeWidthStr;
@@ -64,6 +65,20 @@ pub fn truncate_str<'a, 'b>(s: &'a str, display_width: usize, tail: &'b str) ->
     Cow::from(format!("{}{}", result, result_tail))
 }
 
+pub fn parse_style_sections(s: &str) -> Vec<(ansi_term::Style, &str)> {
+    let mut sections = Vec::new();
+    let mut curr_style = Style::default();
+    for element in AnsiElementIterator::new(s) {
+        match element {
+            Element::Text(start, end) => sections.push((curr_style, &s[start..end])),
+            Element::Csi(style, _, _) => curr_style = style,
+            _ => {}
+        }
+    }
+    sections
+}
+
+// Return the first CSI element, if any, as an `ansi_term::Style`.
 pub fn parse_first_style(s: &str) -> Option<ansi_term::Style> {
     AnsiElementIterator::new(s).find_map(|el| match el {
         Element::Csi(style, _, _) => Some(style),
@@ -108,6 +123,21 @@ pub fn ansi_preserving_slice(s: &str, start: usize) -> String {
         .join("")
 }
 
+/// Return the byte index in `s` of the i-th text byte in `s`. I.e. `i` counts
+/// bytes in non-ANSI-escape-sequence content only.
+pub fn ansi_preserving_index(s: &str, i: usize) -> Option<usize> {
+    let mut index = 0;
+    for element in AnsiElementIterator::new(s) {
+        if let Element::Text(a, b) = element {
+            index += b - a;
+            if index > i {
+                return Some(b - (index - i));
+            }
+        }
+    }
+    None
+}
+
 fn ansi_strings_iterator(s: &str) -> impl Iterator<Item = (&str, bool)> {
     AnsiElementIterator::new(s).map(move |el| match el {
         Element::Csi(_, i, j) => (&s[i..j], true),
@@ -127,6 +157,8 @@ fn strip_ansi_codes_from_strings_iterator<'a>(
 
 #[cfg(test)]
 mod tests {
+    use crate::ansi::ansi_preserving_index;
+
     // Note that src/ansi/console_tests.rs contains additional test coverage for this module.
     use super::{
         ansi_preserving_slice, measure_text_width, parse_first_style,
@@ -190,22 +222,43 @@ mod tests {
     }
 
     #[test]
-    fn test_ansi_preserving_slice() {
+    fn test_ansi_preserving_slice_and_index() {
         assert_eq!(ansi_preserving_slice("", 0), "");
-        assert_eq!(ansi_preserving_slice("a", 0), "a");
-        assert_eq!(ansi_preserving_slice("a", 1), "");
+        assert_eq!(ansi_preserving_index("", 0), None);
+
+        assert_eq!(ansi_preserving_slice("0", 0), "0");
+        assert_eq!(ansi_preserving_index("0", 0), Some(0));
+
+        assert_eq!(ansi_preserving_slice("0", 1), "");
+        assert_eq!(ansi_preserving_index("0", 1), None);
+
+        let raw_string = "\x1b[1;35m0123456789\x1b[0m";
         assert_eq!(
-            ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 1),
-            "\x1b[1;35m2222.2222.2222.2222\x1b[0m"
+            ansi_preserving_slice(raw_string, 1),
+            "\x1b[1;35m123456789\x1b[0m"
         );
+        assert_eq!(ansi_preserving_slice(raw_string, 7), "\x1b[1;35m789\x1b[0m");
+        assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
+        assert_eq!(ansi_preserving_index(raw_string, 1), Some(8));
+        assert_eq!(ansi_preserving_index(raw_string, 7), Some(14));
+
+        let raw_string = "\x1b[1;36m0\x1b[m\x1b[1;36m123456789\x1b[m\n";
         assert_eq!(
-            ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 15),
-            "\x1b[1;35m.2222\x1b[0m"
+            ansi_preserving_slice(raw_string, 1),
+            "\x1b[1;36m\x1b[m\x1b[1;36m123456789\x1b[m\n"
         );
+        assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
+        assert_eq!(ansi_preserving_index(raw_string, 1), Some(18));
+        assert_eq!(ansi_preserving_index(raw_string, 7), Some(24));
+
+        let raw_string = "\x1b[1;36m012345\x1b[m\x1b[1;36m6789\x1b[m\n";
         assert_eq!(
-            ansi_preserving_slice("\x1b[1;36m-\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n", 1),
-            "\x1b[1;36m\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n"
-        )
+            ansi_preserving_slice(raw_string, 3),
+            "\x1b[1;36m345\x1b[m\x1b[1;36m6789\x1b[m\n"
+        );
+        assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
+        assert_eq!(ansi_preserving_index(raw_string, 1), Some(8));
+        assert_eq!(ansi_preserving_index(raw_string, 7), Some(24));
     }
 
     #[test]
diff --git a/src/cli.rs b/src/cli.rs
index aa40ce1..bc92829 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -114,15 +114,19 @@ A complete description of the style string syntax follows:
 COLORS
 ------
 
-There are three ways to specify a color (this section applies to foreground and background colors
+There are four ways to specify a color (this section applies to foreground and background colors
 within a style string):
 
-1. RGB hex code
+1. CSS color name
+
+   Any of the 140 color names used in CSS: https://www.w3schools.com/colors/colors_groups.asp
+
+2. RGB hex code
 
    An example of using an RGB hex code is:
    --file-style=\"#0e7c0e\"
 
-2. ANSI color name
+3. ANSI color name
 
    There are 8 ANSI color names:
    black, red, green, yellow, blue, magenta, cyan, white.
@@ -141,7 +145,7 @@ within a style string):
 
    \"purple\" is accepted as a synonym for \"magenta\". Color names and codes are case-insensitive.
 
-3. ANSI color number
+4. ANSI color number
 
    An example of using an ANSI color number is:
    --file-style=28
@@ -290,6 +294,11 @@ pub struct Opt {
     #[structopt(long = "show-themes")]
     pub show_themes: bool,
 
+    /// Show available named colors. In addition to named colors, arbitrary
+    /// colors can be specified using RGB hex codes. See COLORS section.
+    #[structopt(long = "show-colors")]
+    pub show_colors: bool,
+
     #[structopt(long = "no-gitconfig")]
     /// Do not take any settings from git config. See GIT CONFIG section.
     pub no_gitconfig: bool,
@@ -334,9 +343,9 @@ pub struct Opt {
     /// STYLES section.
     pub minus_emph_style: String,
 
-    #[structopt(long = "minus-non-emph-style", default_value = "auto auto")]
+    #[structopt(long = "minus-non-emph-style", default_value = "minus-style")]
     /// Style (foreground, background, attributes) for non-emphasized sections of removed lines
-    /// that have an emphasized section. Defaults to --minus-style. See STYLES section.
+    /// that have an emphasized section. See STYLES section.
     pub minus_non_emph_style: String,
 
     #[structopt(long = "plus-emph-style", default_value = "syntax auto")]
@@ -344,9 +353,9 @@ pub struct Opt {
     /// STYLES section.
     pub plus_emph_style: String,
 
-    #[structopt(long = "plus-non-emph-style", default_value = "auto auto")]
+    #[structopt(long = "plus-non-emph-style", default_value = "plus-style")]
     /// Style (foreground, background, attributes) for non-emphasized sections of added lines that
-    /// have an emphasized section. Defaults to --plus-style. See STYLES section.
+    /// have an emphasized section. See STYLES section.
     pub plus_non_emph_style: String,
 
     #[structopt(long = "commit-style", default_value = "raw")]
@@ -441,6 +450,40 @@ pub struct Opt {
     )]
     pub blame_timestamp_format: String,
 
+    #[structopt(long = "grep-match-line-style")]
+    /// Style (foreground, background, attributes) for matching lines of code in
+    /// grep output. See STYLES section. Defaults to plus-style.
+    pub grep_match_line_style: Option<String>,
+
+    #[structopt(long = "grep-match-word-style")]
+    /// Style (foreground, background, attributes) for the specific matching
+    /// substrings within a matching line of code in grep output. See STYLES
+    /// section. Defaults to plus-style.
+    pub grep_match_word_style: Option<String>,
+
+    #[structopt(long = "grep-context-line-style")]
+    /// Style (foreground, background, attributes) for non-matching lines of
+    /// code in grep output. See STYLES section. Defaults to zero-style.
+    pub grep_context_line_style: Option<String>,
+
+    #[structopt(long = "grep-file-style")]
+    /// Style (foreground, background, attributes) for file paths in grep
+    /// output. See STYLES section. Defaults to hunk-header-file-path-style.
+    pub grep_file_style: Option<String>,
+
+    #[structopt(long = "grep-line-number-style")]
+    /// Style (foreground, background, attributes) for line numbers in grep
+    /// output. See STYLES section. Defaults to hunk-header-line-number-style.
+    pub grep_line_number_style: Option<String>,
+
+    #[structopt(long = "grep-separator-symbol", default_value = ":")]
+    /// Symbol used in grep output to separate file path (and line number) from
+    /// the line of file contents. Defaults to ":" for both match and context
+    /// lines, since many terminal emulators recognize constructs like
+    /// "/path/to/file:7:". However, standard grep output uses "-" for context
+    /// lines: set this option to "keep" to keep the original separator symbols.
+    pub grep_separator_symbol: String,
+
     /// Default language used for syntax highlighting when this cannot be
     /// inferred from a filename. It will typically make sense to set this in
     /// per-repository git config (.git/config)
@@ -530,6 +573,10 @@ pub struct Opt {
     #[structopt(long = "wrap-right-prefix-symbol", default_value = "…")]
     pub wrap_right_prefix_symbol: String,
 
+    #[structopt(long = "navigate-regex")]
+    /// A regexp to use in the less pager when navigating (auto-generated when unspecified)
+    pub navigate_regex: Option<String>,
+
     #[structopt(long = "file-modified-label", default_value = "")]
     /// Text to display in front of a modified file path.
     pub file_modified_label: String,
diff --git a/src/color.rs b/src/color.rs
index c816e15..52904b6 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -6,9 +6,10 @@ use lazy_static::lazy_static;
 use syntect::highlighting::Color as SyntectColor;
 
 use crate::fatal;
+use crate::git_config::GitConfig;
 use crate::utils;
 
-pub fn parse_color(s: &str, true_color: bool) -> Option<Color> {
+pub fn parse_color(s: &str, true_color: bool, git_config: Option<&GitConfig>) -> Option<Color> {
     if s == "normal" {
         return None;
     }
@@ -18,11 +19,21 @@ pub fn parse_color(s: &str, true_color: bool) -> Option<Color> {
     let syntect_color = if s.starts_with('#') {
         SyntectColor::from_str(s).unwrap_or_else(|_| die())
     } else {
-        s.parse::<u8>()
+        let syntect_color = s
+            .parse::<u8>()
             .ok()
             .and_then(utils::syntect::syntect_color_from_ansi_number)
             .or_else(|| utils::syntect::syntect_color_from_ansi_name(s))
-            .unwrap_or_else(die)
+            .or_else(|| utils::syntect::syntect_color_from_name(s));
+        if syntect_color.is_none() {
+            if let Some(git_config) = git_config {
+                if let Some(val) = git_config.get::<String>(&format!("delta.{}", s)) {
+                    return parse_color(&val, true_color, None);
+                }
+            }
+            die();
+        }
+        syntect_color.unwrap()
     };
     utils::bat::terminal::to_ansi_color(syntect_color, true_color)
 }
diff --git a/src/colors.rs b/src/colors.rs
new file mode 100644
index 0000000..00e826a
--- /dev/null
+++ b/src/colors.rs
@@ -0,0 +1,213 @@
+use std::collections::HashMap;
+
+pub fn color_groups() -> HashMap<String, Vec<(String, String)>> {
+    [
+        (
+            "Blue",
+            vec![
+                ("cadetblue", "#5f9ea0"),
+                ("steelblue", "#4682b4"),
+                ("lightsteelblue", "#b0c4de"),
+                ("lightblue", "#add8e6"),
+                ("powderblue", "#b0e0e6"),
+                ("lightskyblue", "#87cefa"),
+                ("skyblue", "#87ceeb"),
+                ("cornflowerblue", "#6495ed"),
+                ("deepskyblue", "#00bfff"),
+                ("dodgerblue", "#1e90ff"),
+                ("royalblue", "#4169e1"),
+                ("blue", "#0000ff"),
+                ("mediumblue", "#0000cd"),
+                ("darkblue", "#00008b"),
+                ("navy", "#000080"),
+                ("midnightblue", "#191970"),
+            ],
+        ),
+        (
+            "Brown",
+            vec![
+                ("cornsilk", "#fff8dc"),
+                ("blanchedalmond", "#ffebcd"),
+                ("bisque", "#ffe4c4"),
+                ("navajowhite", "#ffdead"),
+                ("wheat", "#f5deb3"),
+                ("burlywood", "#deb887"),
+                ("tan", "#d2b48c"),
+                ("rosybrown", "#bc8f8f"),
+                ("sandybrown", "#f4a460"),
+                ("goldenrod", "#daa520"),
+                ("darkgoldenrod", "#b8860b"),
+                ("peru", "#cd853f"),
+                ("chocolate", "#d2691e"),
+                ("olive", "#808000"),
+                ("saddlebrown", "#8b4513"),
+                ("sienna", "#a0522d"),
+                ("brown", "#a52a2a"),
+                ("maroon", "#800000"),
+            ],
+        ),
+        (
+            "Cyan",
+            vec![
+                ("aqua", "#00ffff"),
+                ("cyan", "#00ffff"),
+                ("lightcyan", "#e0ffff"),
+                ("paleturquoise", "#afeeee"),
+                ("aquamarine", "#7fffd4"),
+                ("turquoise", "#40e0d0"),
+                ("mediumturquoise", "#48d1cc"),
+                ("darkturquoise", "#00ced1"),
+            ],
+        ),
+        (
+            "Green",
+            vec![
+                ("greenyellow", "#adff2f"),
+                ("chartreuse", "#7fff00"),
+                ("lawngreen", "#7cfc00"),
+                ("lime", "#00ff00"),
+                ("limegreen", "#32cd32"),
+                ("palegreen", "#98fb98"),
+                ("lightgreen", "#90ee90"),
+                ("mediumspringgreen", "#00fa9a"),
+                ("springgreen", "#00ff7f"),
+                ("mediumseagreen", "#3cb371"),
+                ("seagreen", "#2e8b57"),
+                ("forestgreen", "#228b22"),
+                ("green", "#008000"),
+                ("darkgreen", "#006400"),
+                ("yellowgreen", "#9acd32"),
+                ("olivedrab", "#6b8e23"),
+                ("darkolivegreen", "#556b2f"),
+                ("mediumaquamarine", "#66cdaa"),
+                ("darkseagreen", "#8fbc8f"),
+                ("lightseagreen", "#20b2aa"),
+                ("darkcyan", "#008b8b"),
+                ("teal", "#008080"),
+            ],
+        ),
+        (
+            "Grey",
+            vec![
+                ("gainsboro", "#dcdcdc"),
+                ("lightgray", "#d3d3d3"),
+                ("silver", "#c0c0c0"),
+                ("darkgray", "#a9a9a9"),
+                ("dimgray", "#696969"),
+                ("gray", "#808080"),
+                ("lightslategray", "#778899"),
+                ("slategray", "#708090"),
+                ("darkslategray", "#2f4f4f"),
+                ("black", "#000000"),
+            ],
+        ),
+        (
+            "Orange",
+            vec![
+                ("orange", "#ffa500"),
+                ("darkorange", "#ff8c00"),
+                ("coral", "#ff7f50"),
+                ("tomato", "#ff6347"),
+                ("orangered", "#ff4500"),
+            ],
+        ),
+        (
+            "Pink",
+            vec![
+                ("pink", "#ffc0cb"),
+                ("lightpink", "#ffb6c1"),
+                ("hotpink", "#ff69b4"),
+                ("deeppink", "#ff1493"),
+                ("palevioletred", "#db7093"),
+                ("mediumvioletred", "#c71585"),
+            ],
+        ),
+        (
+            "Purple",
+            vec![
+                ("lavender", "#e6e6fa"),
+                ("thistle", "#d8bfd8"),
+                ("plum", "#dda0dd"),
+                ("orchid", "#da70d6"),
+                ("violet", "#ee82ee"),
+                ("fuchsia", "#ff00ff"),
+                ("magenta", "#ff00ff"),
+                ("mediumorchid", "#ba55d3"),
+                ("darkorchid", "#9932cc"),
+                ("darkviolet", "#9400d3"),
+                ("blueviolet", "#8a2be2"),
+                ("darkmagenta", "#8b008b"),
+                ("purple", "#800080"),
+                ("mediumpurple", "#9370db"),
+                ("mediumslateblue", "#7b68ee"),
+                ("slateblue", "#6a5acd"),
+                ("darkslateblue", "#483d8b"),
+                ("rebeccapurple", "#663399"),
+                ("indigo", "#4b0082"),
+            ],
+        ),
+        (
+            "Red",
+            vec![
+                ("lightsalmon", "#ffa07a"),
+                ("salmon", "#fa8072"),
+                ("darksalmon", "#e9967a"),
+                ("lightcoral", "#f08080"),
+                ("indianred", "#cd5c5c"),
+                ("crimson", "#dc143c"),
+                ("red", "#ff0000"),
+                ("firebrick", "#b22222"),
+                ("darkred", "#8b0000"),
+            ],
+        ),
+        (
+            "White",
+            vec![
+                ("white", "#ffffff"),
+                ("snow", "#fffafa"),
+                ("honeydew", "#f0fff0"),
+                ("mintcream", "#f5fffa"),
+                ("azure", "#f0ffff"),
+                ("aliceblue", "#f0f8ff"),
+                ("ghostwhite", "#f8f8ff"),
+                ("whitesmoke", "#f5f5f5"),
+                ("seashell", "#fff5ee"),
+                ("beige", "#f5f5dc"),
+                ("oldlace", "#fdf5e6"),
+                ("floralwhite", "#fffaf0"),
+                ("ivory", "#fffff0"),
+                ("antiquewhite", "#faebd7"),
+                ("linen", "#faf0e6"),
+                ("lavenderblush", "#fff0f5"),
+                ("mistyrose", "#ffe4e1"),
+            ],
+        ),
+        (
+            "Yellow",
+            vec![
+                ("gold", "#ffd700"),
+                ("yellow", "#ffff00"),
+                ("lightyellow", "#ffffe0"),
+                ("lemonchiffon", "#fffacd"),
+                ("lightgoldenrodyellow", "#fafad2"),
+                ("papayawhip", "#ffefd5"),
+                ("moccasin", "#ffe4b5"),
+                ("peachpuff", "#ffdab9"),
+                ("palegoldenrod", "#eee8aa"),
+                ("khaki", "#f0e68c"),
+                ("darkkhaki", "#bdb76b"),
+            ],
+        ),
+    ]
+    .iter()
+    .map(|(name, colors)| {
+        (
+            name.to_string(),
+            colors
+                .iter()
+                .map(|(color, hex)| (color.to_string(), hex.to_string()))
+                .collect(),
+        )
+    })
+    .collect()
+}
diff --git a/src/config.rs b/src/config.rs
index 4ed7f27..1c28d06 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -19,7 +19,8 @@ use crate::features::side_by_side::{self, ansifill, LeftRight};
 use crate::git_config::{GitConfig, GitConfigEntry};
 use crate::minusplus::MinusPlus;
 use crate::paint::BgFillMethod;
-use crate::style::{self, Style};
+use crate::parse_styles;
+use crate::style::Style;
 use crate::tests::TESTING;
 use crate::utils::bat::output::PagingMode;
 use crate::utils::syntect::FromDeltaStyle;
@@ -79,6 +80,12 @@ pub struct Config {
     pub git_config: Option<GitConfig>,
     pub git_minus_style: Style,
     pub git_plus_style: Style,
+    pub grep_context_line_style: Style,
+    pub grep_file_style: Style,
+    pub grep_line_number_style: Style,
+    pub grep_match_line_style: Style,
+    pub grep_match_word_style: Style,
+    pub grep_separator_symbol: String,
     pub hunk_header_file_style: Style,
     pub hunk_header_line_number_style: Style,
     pub hunk_header_style_include_file_path: bool,
@@ -106,7 +113,7 @@ pub struct Config {
     pub minus_file: Option<PathBuf>,
     pub minus_non_emph_style: Style,
     pub minus_style: Style,
-    pub navigate_regexp: Option<String>,
+    pub navigate_regex: Option<String>,
     pub navigate: bool,
     pub null_style: Style,
     pub null_syntect_style: SyntectStyle,
@@ -149,34 +156,7 @@ impl Config {
 
 impl From<cli::Opt> for Config {
     fn from(opt: cli::Opt) -> Self {
-        let (
-            minus_style,
-            minus_emph_style,
-            minus_non_emph_style,
-            minus_empty_line_marker_style,
-            zero_style,
-            plus_style,
-            plus_emph_style,
-            plus_non_emph_style,
-            plus_empty_line_marker_style,
-            whitespace_error_style,
-        ) = make_hunk_styles(&opt);
-
-        let (
-            commit_style,
-            file_style,
-            hunk_header_style,
-            hunk_header_file_style,
-            hunk_header_line_number_style,
-        ) = make_commit_file_hunk_header_styles(&opt);
-
-        let (
-            line_numbers_minus_style,
-            line_numbers_zero_style,
-            line_numbers_plus_style,
-            line_numbers_left_style,
-            line_numbers_right_style,
-        ) = make_line_number_styles(&opt);
+        let styles = parse_styles::parse_styles(&opt);
 
         let max_line_distance_for_naively_paired_lines =
             env::get_env_var("DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES")
@@ -201,22 +181,6 @@ impl From<cli::Opt> for Config {
             ));
         });
 
-        let inline_hint_style = Style::from_str(
-            &opt.inline_hint_style,
-            None,
-            None,
-            opt.computed.true_color,
-            false,
-        );
-        let git_minus_style = match opt.git_config_entries.get("color.diff.old") {
-            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
-            _ => *style::GIT_DEFAULT_MINUS_STYLE,
-        };
-        let git_plus_style = match opt.git_config_entries.get("color.diff.new") {
-            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
-            _ => *style::GIT_DEFAULT_PLUS_STYLE,
-        };
-
         let blame_palette = make_blame_palette(opt.blame_palette, opt.computed.is_light_mode);
 
         let file_added_label = opt.file_added_label;
@@ -244,8 +208,10 @@ impl From<cli::Opt> for Config {
             side_by_side_data,
         );
 
-        let navigate_regexp = if opt.navigate || opt.show_themes {
-            Some(navigate::make_navigate_regexp(
+        let navigate_regex = if (opt.navigate || opt.show_themes)
+            && (opt.navigate_regex.is_none() || opt.navigate_regex == Some("".to_string()))
+        {
+            Some(navigate::make_navigate_regex(
                 opt.show_themes,
                 &file_modified_label,
                 &file_added_label,
@@ -254,7 +220,7 @@ impl From<cli::Opt> for Config {
                 &hunk_label,
             ))
         } else {
-            None
+            opt.navigate_regex
         };
 
         let wrap_max_lines_plus1 = adapt_wrap_max_lines_argument(opt.wrap_max_lines);
@@ -267,7 +233,7 @@ impl From<cli::Opt> for Config {
             blame_format: opt.blame_format,
             blame_palette,
             blame_timestamp_format: opt.blame_timestamp_format,
-            commit_style,
+            commit_style: styles["commit-style"],
             color_only: opt.color_only,
             commit_regex,
             cwd_relative_to_repo_root: std::env::var("GIT_PREFIX").ok(),
@@ -282,12 +248,18 @@ impl From<cli::Opt> for Config {
             file_renamed_label,
             right_arrow,
             hunk_label,
-            file_style,
+            file_style: styles["file-style"],
             git_config: opt.git_config,
             git_config_entries: opt.git_config_entries,
-            hunk_header_file_style,
-            hunk_header_line_number_style,
-            hunk_header_style,
+            grep_context_line_style: styles["grep-context-line-style"],
+            grep_file_style: styles["grep-file-style"],
+            grep_line_number_style: styles["grep-line-number-style"],
+            grep_match_line_style: styles["grep-match-line-style"],
+            grep_match_word_style: styles["grep-match-word-style"],
+            grep_separator_symbol: opt.grep_separator_symbol,
+            hunk_header_file_style: styles["hunk-header-file-style"],
+            hunk_header_line_number_style: styles["hunk-header-line-number-style"],
+            hunk_header_style: styles["hunk-header-style"],
             hunk_header_style_include_file_path: opt
                 .hunk_header_style
                 .split(' ')
@@ -300,7 +272,7 @@ impl From<cli::Opt> for Config {
             hyperlinks_commit_link_format: opt.hyperlinks_commit_link_format,
             hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
             inspect_raw_lines: opt.computed.inspect_raw_lines,
-            inline_hint_style,
+            inline_hint_style: styles["inline-hint-style"],
             keep_plus_minus_markers: opt.keep_plus_minus_markers,
             line_fill_method: if !opt.computed.stdout_is_term && !TESTING {
                 // Don't write ANSI sequences (which rely on the width of the
@@ -316,14 +288,14 @@ impl From<cli::Opt> for Config {
                 opt.line_numbers_right_format,
             ),
             line_numbers_style_leftright: LeftRight::new(
-                line_numbers_left_style,
-                line_numbers_right_style,
+                styles["line-numbers-left-style"],
+                styles["line-numbers-right-style"],
             ),
             line_numbers_style_minusplus: MinusPlus::new(
-                line_numbers_minus_style,
-                line_numbers_plus_style,
+                styles["line-numbers-minus-style"],
+                styles["line-numbers-plus-style"],
             ),
-            line_numbers_zero_style,
+            line_numbers_zero_style: styles["line-numbers-zero-style"],
             line_buffer_size: opt.line_buffer_size,
             max_line_distance: opt.max_line_distance,
             max_line_distance_for_naively_paired_lines,
@@ -344,24 +316,24 @@ impl From<cli::Opt> for Config {
                     )
                 }
             },
-            minus_emph_style,
-            minus_empty_line_marker_style,
+            minus_emph_style: styles["minus-emph-style"],
+            minus_empty_line_marker_style: styles["minus-empty-line-marker-style"],
             minus_file: opt.minus_file,
-            minus_non_emph_style,
-            minus_style,
+            minus_non_emph_style: styles["minus-non-emph-style"],
+            minus_style: styles["minus-style"],
             navigate: opt.navigate,
-            navigate_regexp,
+            navigate_regex,
             null_style: Style::new(),
             null_syntect_style: SyntectStyle::default(),
             pager: opt.pager,
             paging_mode: opt.computed.paging_mode,
-            plus_emph_style,
-            plus_empty_line_marker_style,
+            plus_emph_style: styles["plus-emph-style"],
+            plus_empty_line_marker_style: styles["plus-empty-line-marker-style"],
             plus_file: opt.plus_file,
-            plus_non_emph_style,
-            plus_style,
-            git_minus_style,
-            git_plus_style,
+            plus_non_emph_style: styles["plus-non-emph-style"],
+            plus_style: styles["plus-style"],
+            git_minus_style: styles["git-minus-style"],
+            git_plus_style: styles["git-plus-style"],
             relative_paths: opt.relative_paths,
             show_themes: opt.show_themes,
             side_by_side: opt.side_by_side,
@@ -397,223 +369,16 @@ impl From<cli::Opt> for Config {
                     }
                 },
                 max_lines: wrap_max_lines_plus1,
-                inline_hint_syntect_style: SyntectStyle::from_delta_style(inline_hint_style),
+                inline_hint_syntect_style: SyntectStyle::from_delta_style(
+                    styles["inline-hint-style"],
+                ),
             },
-            whitespace_error_style,
-            zero_style,
+            whitespace_error_style: styles["whitespace-error-style"],
+            zero_style: styles["zero-style"],
         }
     }
 }
 
-fn make_hunk_styles(
-    opt: &cli::Opt,
-) -> (
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-) {
-    let is_light_mode = opt.computed.is_light_mode;
-    let true_color = opt.computed.true_color;
-    let minus_style = Style::from_str(
-        &opt.minus_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let minus_emph_style = Style::from_str(
-        &opt.minus_emph_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_emph_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        true,
-    );
-
-    let minus_non_emph_style = Style::from_str(
-        &opt.minus_non_emph_style,
-        Some(minus_style),
-        None,
-        true_color,
-        false,
-    );
-
-    // The style used to highlight a removed empty line when otherwise it would be invisible due to
-    // lack of background color in minus-style.
-    let minus_empty_line_marker_style = Style::from_str(
-        &opt.minus_empty_line_marker_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let zero_style = Style::from_str(&opt.zero_style, None, None, true_color, false);
-
-    let plus_style = Style::from_str(
-        &opt.plus_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let plus_emph_style = Style::from_str(
-        &opt.plus_emph_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_emph_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        true,
-    );
-
-    let plus_non_emph_style = Style::from_str(
-        &opt.plus_non_emph_style,
-        Some(plus_style),
-        None,
-        true_color,
-        false,
-    );
-
-    // The style used to highlight an added empty line when otherwise it would be invisible due to
-    // lack of background color in plus-style.
-    let plus_empty_line_marker_style = Style::from_str(
-        &opt.plus_empty_line_marker_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let whitespace_error_style =
-        Style::from_str(&opt.whitespace_error_style, None, None, true_color, false);
-
-    (
-        minus_style,
-        minus_emph_style,
-        minus_non_emph_style,
-        minus_empty_line_marker_style,
-        zero_style,
-        plus_style,
-        plus_emph_style,
-        plus_non_emph_style,
-        plus_empty_line_marker_style,
-        whitespace_error_style,
-    )
-}
-
-fn make_line_number_styles(opt: &cli::Opt) -> (Style, Style, Style, Style, Style) {
-    let true_color = opt.computed.true_color;
-    let line_numbers_left_style =
-        Style::from_str(&opt.line_numbers_left_style, None, None, true_color, false);
-
-    let line_numbers_minus_style =
-        Style::from_str(&opt.line_numbers_minus_style, None, None, true_color, false);
-
-    let line_numbers_zero_style =
-        Style::from_str(&opt.line_numbers_zero_style, None, None, true_color, false);
-
-    let line_numbers_plus_style =
-        Style::from_str(&opt.line_numbers_plus_style, None, None, true_color, false);
-
-    let line_numbers_right_style =
-        Style::from_str(&opt.line_numbers_right_style, None, None, true_color, false);
-
-    (
-        line_numbers_minus_style,
-        line_numbers_zero_style,
-        line_numbers_plus_style,
-        line_numbers_left_style,
-        line_numbers_right_style,
-    )
-}
-
-fn make_commit_file_hunk_header_styles(opt: &cli::Opt) -> (Style, Style, Style, Style, Style) {
-    let true_color = opt.computed.true_color;
-    (
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.commit_style,
-            None,
-            Some(&opt.commit_decoration_style),
-            opt.deprecated_commit_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.file_style,
-            None,
-            Some(&opt.file_decoration_style),
-            opt.deprecated_file_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.hunk_header_style,
-            None,
-            Some(&opt.hunk_header_decoration_style),
-            opt.deprecated_hunk_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes(
-            &opt.hunk_header_file_style,
-            None,
-            None,
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes(
-            &opt.hunk_header_line_number_style,
-            None,
-            None,
-            true_color,
-            false,
-        ),
-    )
-}
-
 fn make_blame_palette(blame_palette: Option<String>, is_light_mode: bool) -> Vec<String> {
     match (blame_palette, is_light_mode) {
         (Some(string), _) => string
diff --git a/src/delta.rs b/src/delta.rs
index fbf45c5..fa983a6 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -23,6 +23,8 @@ pub enum State {
     SubmoduleLog, // In a submodule section, with gitconfig diff.submodule = log
     SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short
     Blame(String, Option<String>), // In a line of `git blame` output (commit, repeat_blame_line).
+    GitShowFile, // In a line of `git show $revision:./path/to/file.ext` output
+    Grep,     // In a line of `git grep` output
     Unknown,
     // The following elements are created when a line is wrapped to display it:
     HunkZeroWrapped,  // Wrapped unchanged line
@@ -120,7 +122,9 @@ impl<'a> StateMachine<'a> {
                 || self.handle_submodule_log_line()?
                 || self.handle_submodule_short_line()?
                 || self.handle_hunk_line()?
+                || self.handle_git_show_file_line()?
                 || self.handle_blame_line()?
+                || self.handle_grep_line()?
                 || self.should_skip_line()
                 || self.emit_line_unchanged()?;
         }
@@ -133,7 +137,13 @@ impl<'a> StateMachine<'a> {
     fn ingest_line(&mut self, raw_line_bytes: &[u8]) {
         // TODO: retain raw_line as Cow
         self.raw_line = String::from_utf8_lossy(raw_line_bytes).to_string();
-        if self.config.max_line_length > 0 && self.raw_line.len() > self.config.max_line_length {
+        if self.config.max_line_length > 0
+            && self.raw_line.len() > self.config.max_line_length
+            // We must not truncate ripgrep --json output
+            // TODO: An alternative might be to truncate `line` but retain
+            // `raw_line` untruncated?
+            && !self.raw_line.starts_with('{')
+        {
             self.raw_line = ansi::truncate_str(
                 &self.raw_line,
                 self.config.max_line_length,
diff --git a/src/features/navigate.rs b/src/features/navigate.rs
index cb5fb14..6018290 100644
--- a/src/features/navigate.rs
+++ b/src/features/navigate.rs
@@ -30,7 +30,7 @@ pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
 }
 
 // Construct the regexp used by less for paging, if --show-themes or --navigate is enabled.
-pub fn make_navigate_regexp(
+pub fn make_navigate_regex(
     show_themes: bool,
     file_modified_label: &str,
     file_added_label: &str,
@@ -41,27 +41,34 @@ pub fn make_navigate_regexp(
     if show_themes {
         "^Theme:".to_string()
     } else {
+        let optional_regexp = |find: &str| {
+            if !find.is_empty() {
+                format!("|{}", regex::escape(find))
+            } else {
+                "".to_string()
+            }
+        };
         format!(
-            "^(commit|{}|{}|{}|{}|{})",
-            regex::escape(file_added_label),
-            regex::escape(file_removed_label),
-            regex::escape(file_renamed_label),
-            regex::escape(file_modified_label),
-            regex::escape(hunk_label),
+            "^(commit{}{}{}{}{})",
+            optional_regexp(file_added_label),
+            optional_regexp(file_removed_label),
+            optional_regexp(file_renamed_label),
+            optional_regexp(file_modified_label),
+            optional_regexp(hunk_label),
         )
     }
 }
 
 // Create a less history file to be used by delta's child less process. This file is initialized
-// with the contents of user's real less hist file, to which the navigate regexp is appended. This
-// has the effect that 'n' or 'N' in delta's less process will search for the navigate regexp,
+// with the contents of user's real less hist file, to which the navigate regex is appended. This
+// has the effect that 'n' or 'N' in delta's less process will search for the navigate regex,
 // without the undesirable aspects of using --pattern, yet without polluting the user's less search
-// history with delta's navigate regexp. See
+// history with delta's navigate regex. See
 // https://github.com/dandavison/delta/issues/237#issuecomment-780654036. Note that with the
 // current implementation, no writes to the delta less history file are propagated back to the real
 // history file so, for example, a (non-navigate) search performed in the delta less process will
 // not be stored in history.
-pub fn copy_less_hist_file_and_append_navigate_regexp(config: &Config) -> std::io::Result<PathBuf> {
+pub fn copy_less_hist_file_and_append_navigate_regex(config: &Config) -> std::io::Result<PathBuf> {
     let delta_less_hist_file = get_delta_less_hist_file()?;
     let initial_contents = ".less-history-file:\n".to_string();
     let mut contents = if let Some(hist_file) = get_less_hist_file() {
@@ -76,7 +83,7 @@ pub fn copy_less_hist_file_and_append_navigate_regexp(config: &Config) -> std::i
         std::fs::File::create(&delta_less_hist_file)?,
         "{}\"{}",
         contents,
-        config.navigate_regexp.as_ref().unwrap(),
+        config.navigate_regex.as_ref().unwrap(),
     )?;
     Ok(delta_less_hist_file)
 }
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs
index e62332f..629dc70 100644
--- a/src/handlers/blame.rs
+++ b/src/handlers/blame.rs
@@ -10,6 +10,7 @@ use crate::config::delta_unreachable;
 use crate::delta::{self, State, StateMachine};
 use crate::format::{self, Placeholder};
 use crate::paint::BgShouldFill;
+use crate::paint::StyleSectionSpecifier;
 use crate::style::Style;
 use crate::utils;
 
@@ -35,7 +36,10 @@ impl<'a> StateMachine<'a> {
             {
                 let is_repeat = previous_commit == Some(blame.commit);
                 let color = self.get_color(blame.commit, previous_commit, is_repeat);
-                let mut style = Style::from_colors(None, color::parse_color(&color, true));
+                let mut style = Style::from_colors(
+                    None,
+                    color::parse_color(&color, true, self.config.git_config.as_ref()),
+                );
                 // TODO: This will often be pointlessly updating a key with the
                 // value it already has. It might be nicer to do this (and
                 // compute the style) in get_color(), but as things stand the
@@ -84,7 +88,7 @@ impl<'a> StateMachine<'a> {
                 self.state = State::Blame(blame.commit.to_owned(), repeat_blame_line.to_owned());
                 self.painter.syntax_highlight_and_paint_line(
                     &format!("{}\n", blame.code),
-                    style,
+                    StyleSectionSpecifier::Style(style),
                     self.state.clone(),
                     BgShouldFill::default(),
                 );
diff --git a/src/handlers/file_meta.rs b/src/handlers/file_meta.rs
index 8a1ea0d..bea0562 100644
--- a/src/handlers/file_meta.rs
+++ b/src/handlers/file_meta.rs
@@ -210,7 +210,7 @@ fn get_file_extension_from_file_meta_line_file_path(path: &str) -> Option<&str>
 }
 
 /// Attempt to parse input as a file path and return extension as a &str.
-fn get_extension(s: &str) -> Option<&str> {
+pub fn get_extension(s: &str) -> Option<&str> {
     let path = Path::new(s);
     path.extension()
         .and_then(|e| e.to_str())
diff --git a/src/handlers/git_show_file.rs b/src/handlers/git_show_file.rs
new file mode 100644
index 0000000..2a4998c
--- /dev/null
+++ b/src/handlers/git_show_file.rs
@@ -0,0 +1,33 @@
+use crate::delta::{State, StateMachine};
+use crate::paint::{BgShouldFill, StyleSectionSpecifier};
+use crate::utils::process;
+
+impl<'a> StateMachine<'a> {
+    // If this is a line of `git show $revision:/path/to/file.ext` output then
+    // syntax-highlight it as language `ext`.
+    pub fn handle_git_show_file_line(&mut self) -> std::io::Result<bool> {
+        self.painter.emit()?;
+        let mut handled_line = false;
+        if matches!(self.state, State::Unknown) {
+            if let Some(process::CallingProcess::GitShow(extension)) =
+                process::calling_process().as_deref()
+            {
+                self.state = State::GitShowFile;
+                self.painter.set_syntax(Some(extension));
+                self.painter.set_highlighter();
+            } else {
+                return Ok(handled_line);
+            }
+        }
+        if matches!(self.state, State::GitShowFile) {
+            self.painter.syntax_highlight_and_paint_line(
+                &self.line,
+                StyleSectionSpecifier::Style(self.config.zero_style),
+                self.state.clone(),
+                BgShouldFill::default(),
+            );
+            handled_line = true;
+        }
+        Ok(handled_line)
+    }
+}
diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs
new file mode 100644
index 0000000..a7e57d1
--- /dev/null
+++ b/src/handlers/grep.rs
@@ -0,0 +1,829 @@
+use std::borrow::Cow;
+
+use lazy_static::lazy_static;
+use regex::Regex;
+use serde::Deserialize;
+use unicode_segmentation::UnicodeSegmentation;
+
+use crate::ansi;
+use crate::delta::{State, StateMachine};
+use crate::handlers::{self, ripgrep_json};
+use crate::paint::{self, BgShouldFill, StyleSectionSpecifier};
+use crate::style::Style;
+use crate::utils::process;
+
+#[derive(Debug, PartialEq)]
+pub struct GrepLine<'b> {
+    pub path: Cow<'b, str>,
+    pub line_number: Option<usize>,
+    pub line_type: LineType,
+    pub code: Cow<'b, str>,
+    pub submatches: Option<Vec<(usize, usize)>>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum LineType {
+    ContextHeader,
+    Context,
+    Match,
+    Ignore,
+}
+
+struct GrepOutputConfig {
+    add_navigate_marker_to_matches: bool,
+    render_context_header_as_hunk_header: bool,
+    pad_line_number: bool,
+}
+
+lazy_static! {
+    static ref OUTPUT_CONFIG: GrepOutputConfig = make_output_config();
+}
+
+impl<'a> StateMachine<'a> {
+    // If this is a line of git grep output then render it accordingly.
+    pub fn handle_grep_line(&mut self) -> std::io::Result<bool> {
+        self.painter.emit()?;
+        let mut handled_line = false;
+
+        let try_parse = matches!(&self.state, State::Grep | State::Unknown);
+
+        if try_parse {
+            if let Some(mut grep_line) = parse_grep_line(&self.line) {
+                if matches!(grep_line.line_type, LineType::Ignore) {
+                    handled_line = true;
+                    return Ok(handled_line);
+                }
+
+                // Emit syntax-highlighted code
+                // TODO: Determine the language less frequently, e.g. only when the file changes.
+                if let Some(lang) = handlers::file_meta::get_extension(&grep_line.path)
+                    .or_else(|| self.config.default_language.as_deref())
+                {
+                    self.painter.set_syntax(Some(lang));
+                    self.painter.set_highlighter();
+                }
+                self.state = State::Grep;
+
+                match (
+                    &grep_line.line_type,
+                    OUTPUT_CONFIG.render_context_header_as_hunk_header,
+                ) {
+                    // Emit context header line
+                    (LineType::ContextHeader, true) => handlers::hunk_header::write_hunk_header(
+                        &grep_line.code,
+                        &[(grep_line.line_number.unwrap_or(0), 0)],
+                        &mut self.painter,
+                        &self.line,
+                        &grep_line.path,
+                        self.config,
+                    )?,
+                    _ => {
+                        if self.config.navigate {
+                            write!(
+                                self.painter.writer,
+                                "{}",
+                                match (
+                                    &grep_line.line_type,
+                                    OUTPUT_CONFIG.add_navigate_marker_to_matches
+                                ) {
+                                    (LineType::Match, true) => "• ",
+                                    (_, true) => "  ",
+                                    _ => "",
+                                }
+                            )?
+                        }
+                        // Emit file & line-number
+                        let separator = if self.config.grep_separator_symbol == "keep" {
+                            // grep, rg, and git grep use ":" for matching lines
+                            // and "-" for non-matching lines (and `git grep -W`
+                            // uses "=" for a context header line).
+                            match grep_line.line_type {
+                                LineType::Match => ":",
+                                LineType::Context => "-",
+                                LineType::ContextHeader => "=",
+                                LineType::Ignore => "",
+                            }
+                        } else {
+                            // But ":" results in a "file/path:number:"
+                            // construct that terminal emulators are more likely
+                            // to recognize and render as a clickable link. If
+                            // navigate is enabled then there is already a good
+                            // visual indicator of match lines (in addition to
+                            // the grep-match-style highlighting) and so we use
+                            // ":" for matches and non-matches alike.
+                            &self.config.grep_separator_symbol
+                        };
+                        write!(
+                            self.painter.writer,
+                            "{}",
+                            paint::paint_file_path_with_line_number(
+                                grep_line.line_number,
+                                &grep_line.path,
+                                OUTPUT_CONFIG.pad_line_number,
+                                separator,
+                                true,
+                                Some(self.config.grep_file_style),
+                                Some(self.config.grep_line_number_style),
+                                self.config
+                            )
+                        )?;
+
+                        // Emit code line
+                        let code_style_sections =
+                            match (&grep_line.line_type, &grep_line.submatches) {
+                                (LineType::Match, Some(submatches)) => {
+                                    // We expand tabs at this late stage because
+                                    // the tabs are escaped in the JSON, so
+                                    // expansion must come after JSON parsing.
+                                    // (At the time of writing, we are in this
+                                    // arm iff we are handling `ripgrep --json`
+                                    // output.)
+                                    grep_line.code = self
+                                        .painter
+                                        .expand_tabs(grep_line.code.graphemes(true))
+                                        .into();
+                                    make_style_sections(
+                                        &grep_line.code,
+                                        submatches,
+                                        self.config.grep_match_word_style,
+                                        self.config.grep_match_line_style,
+                                    )
+                                }
+                                (LineType::Match, None) => {
+                                    // HACK: We need tabs expanded, and we need
+                                    // the &str passed to
+                                    // `get_code_style_sections` to live long
+                                    // enough. But at the point it is guaranteed
+                                    // that this handler is going to handle this
+                                    // line, so mutating it is acceptable.
+                                    self.raw_line =
+                                        self.painter.expand_tabs(self.raw_line.graphemes(true));
+                                    get_code_style_sections(
+                                        &self.raw_line,
+                                        self.config.grep_match_word_style,
+                                        self.config.grep_match_line_style,
+                                        &grep_line,
+                                    )
+                                    .unwrap_or(
+                                        StyleSectionSpecifier::Style(
+                                            self.config.grep_match_line_style,
+                                        ),
+                                    )
+                                }
+                                _ => StyleSectionSpecifier::Style(
+                                    self.config.grep_context_line_style,
+                                ),
+                            };
+                        self.painter.syntax_highlight_and_paint_line(
+                            &format!("{}\n", grep_line.code),
+                            code_style_sections,
+                            self.state.clone(),
+                            BgShouldFill::default(),
+                        )
+                    }
+                }
+                handled_line = true
+            }
+        }
+        Ok(handled_line)
+    }
+}
+
+fn make_style_sections<'a>(
+    line: &'a str,
+    submatches: &[(usize, usize)],
+    match_style: Style,
+    non_match_style: Style,
+) -> StyleSectionSpecifier<'a> {
+    let mut sections = Vec::new();
+    let mut curr = 0;
+    for (start_, end_) in submatches {
+        let (start, end) = (*start_, *end_);
+        if start > curr {
+            sections.push((non_match_style, &line[curr..start]))
+        };
+        sections.push((match_style, &line[start..end]));
+        curr = end;
+    }
+    if curr < line.len() {
+        sections.push((non_match_style, &line[curr..]))
+    }
+    StyleSectionSpecifier::StyleSections(sections)
+}
+
+// Return style sections describing colors received from git.
+fn get_code_style_sections<'b>(
+    raw_line: &'b str,
+    match_style: Style,
+    non_match_style: Style,
+    grep: &GrepLine,
+) -> Option<StyleSectionSpecifier<'b>> {
+    if let Some(raw_code_start) = ansi::ansi_preserving_index(
+        raw_line,
+        match grep.line_number {
+            Some(n) => format!("{}:{}:", grep.path, n).len(),
+            None => grep.path.len() + 1,
+        },
+    ) {
+        let match_style_sections = ansi::parse_style_sections(&raw_line[raw_code_start..])
+            .iter()
+            .map(|(ansi_term_style, s)| {
+                if ansi_term_style.foreground.is_some() {
+                    (match_style, *s)
+                } else {
+                    (non_match_style, *s)
+                }
+            })
+            .collect();
+        Some(StyleSectionSpecifier::StyleSections(match_style_sections))
+    } else {
+        None
+    }
+}
+
+fn make_output_config() -> GrepOutputConfig {
+    match process::calling_process().as_deref() {
+        Some(process::CallingProcess::GitGrep((longs, shorts)))
+            if shorts.contains("-W") || longs.contains("--function-context") =>
+        {
+            // --function-context is in effect: i.e. the entire function is
+            // being displayed. In that case we don't render the first line as a
+            // header, since the second line is the true next line, and it will
+            // be more readable to have these displayed normally. We do add the
+            // navigate marker, since match lines will be surrounded by (many)
+            // non-match lines. And, since we are printing (many) successive lines
+            // of code, we pad line numbers <100 in order to maintain code
+            // alignment up to line 9999.
+            GrepOutputConfig {
+                render_context_header_as_hunk_header: false,
+                add_navigate_marker_to_matches: true,
+                pad_line_number: true,
+            }
+        }
+        Some(process::CallingProcess::GitGrep((longs, shorts)))
+            if shorts.contains("-p") || longs.contains("--show-function") =>
+        {
+            // --show-function is in effect, i.e. the function header is being
+            // displayed, along with matches within the function. Therefore we
+            // render the first line as a header, but we do not add the navigate
+            // marker, since all non-header lines are matches.
+            GrepOutputConfig {
+                render_context_header_as_hunk_header: true,
+                add_navigate_marker_to_matches: false,
+                pad_line_number: false,
+            }
+        }
+        _ => GrepOutputConfig {
+            render_context_header_as_hunk_header: true,
+            add_navigate_marker_to_matches: false,
+            pad_line_number: false,
+        },
+    }
+}
+
+enum GrepLineRegex {
+    FilePathWithFileExtension,
+    FilePathWithoutSeparatorCharacters,
+}
+
+lazy_static! {
+    static ref GREP_LINE_REGEX_ASSUMING_FILE_EXTENSION: Regex =
+        make_grep_line_regex(GrepLineRegex::FilePathWithFileExtension);
+}
+
+lazy_static! {
+    static ref GREP_LINE_REGEX_ASSUMING_NO_INTERNAL_SEPARATOR_CHARS: Regex =
+        make_grep_line_regex(GrepLineRegex::FilePathWithoutSeparatorCharacters);
+}
+
+// See tests for example grep lines
+fn make_grep_line_regex(regex_variant: GrepLineRegex) -> Regex {
+    // Grep tools such as `git grep` and `rg` emit lines like the following,
+    // where "xxx" represents arbitrary code. Note that there are 3 possible
+    // "separator characters": ':', '-', '='.
+
+    // The format is ambiguous, but we attempt to parse it.
+
+    // src/co-7-fig.rs:xxx
+    // src/co-7-fig.rs:7:xxx
+    // src/co-7-fig.rs-xxx
+    // src/co-7-fig.rs-7-xxx
+    // src/co-7-fig.rs=xxx
+    // src/co-7-fig.rs=7=xxx
+
+    // Makefile:xxx
+    // Makefile:7:xxx
+    // Makefile-xxx
+    // Makefile-7-xxx
+
+    // Make-7-file:xxx
+    // Make-7-file:7:xxx
+    // Make-7-file-xxx
+    // Make-7-file-7-xxx
+
+    let file_path = match regex_variant {
+        GrepLineRegex::FilePathWithFileExtension => {
+            r"
+        (                        # 1. file name (colons not allowed)
+            [^:|\ ]                 # try to be strict about what a file path can start with
+            [^:]*                   # anything
+            \.[^.\ :=-]{1,6}        # extension
+        )    
+        "
+        }
+        GrepLineRegex::FilePathWithoutSeparatorCharacters => {
+            r"
+        (                        # 1. file name (colons not allowed)
+            [^:|\ =-]               # try to be strict about what a file path can start with
+            [^:=-]*                 # anything except separators
+            [^:\ ]                  # a file name cannot end with whitespace
+        )    
+        "
+        }
+    };
+
+    Regex::new(&format!(
+        "(?x)
+^
+{file_path}
+(?:
+    (                    
+        :                # 2. match marker
+        (?:([0-9]+):)?   # 3. optional: line number followed by second match marker
+    )
+    |
+    (
+        -                # 4. nomatch marker
+        (?:([0-9]+)-)?   # 5. optional: line number followed by second nomatch marker
+    )
+    |
+    (
+        =                # 6. match marker
+        (?:([0-9]+)=)?   # 7. optional: line number followed by second header marker
+    )
+)
+(.*)                     # 8. code (i.e. line contents)
+$
+",
+        file_path = file_path
+    ))
+    .unwrap()
+}
+
+pub fn parse_grep_line(line: &str) -> Option<GrepLine> {
+    if line.starts_with('{') {
+        ripgrep_json::parse_line(line)
+    } else {
+        match process::calling_process().as_deref() {
+            Some(process::CallingProcess::GitGrep(_))
+            | Some(process::CallingProcess::OtherGrep) => [
+                &*GREP_LINE_REGEX_ASSUMING_FILE_EXTENSION,
+                &*GREP_LINE_REGEX_ASSUMING_NO_INTERNAL_SEPARATOR_CHARS,
+            ]
+            .iter()
+            .find_map(|regex| _parse_grep_line(*regex, line)),
+            _ => None,
+        }
+    }
+}
+
+pub fn _parse_grep_line<'b>(regex: &Regex, line: &'b str) -> Option<GrepLine<'b>> {
+    let caps = regex.captures(line)?;
+    let file = caps.get(1).unwrap().as_str().into();
+    let (line_type, line_number) = &[
+        (2, LineType::Match),
+        (4, LineType::Context),
+        (6, LineType::ContextHeader),
+    ]
+    .iter()
+    .find_map(|(i, line_type)| {
+        if caps.get(*i).is_some() {
+            let line_number: Option<usize> =
+                caps.get(i + 1).map(|m| m.as_str().parse().ok()).flatten();
+            Some((*line_type, line_number))
+        } else {
+            None
+        }
+    })
+    .unwrap(); // The regex matches so one of the three alternatrives must have matched
+    let code = caps.get(8).unwrap().as_str().into();
+
+    Some(GrepLine {
+        path: file,
+        line_number: *line_number,
+        line_type: *line_type,
+        code,
+        submatches: None,
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::handlers::grep::{parse_grep_line, GrepLine, LineType};
+    use crate::utils::process::tests::cfg;
+
+    #[test]
+    fn test_parse_grep_match() {
+        let fake_parent_grep_command = "git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/co-7-fig.rs:xxx"),
+                Some(GrepLine {
+                    path: "src/co-7-fig.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs:use crate::minusplus::MinusPlus;"),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "use crate::minusplus::MinusPlus;".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line(
+                    "src/config.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+                ),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/con-fig.rs:use crate::minusplus::MinusPlus;"),
+                Some(GrepLine {
+                    path: "src/con-fig.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "use crate::minusplus::MinusPlus;".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line(
+                    "src/con-fig.rs:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+                ),
+                Some(GrepLine {
+                    path: "src/con-fig.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+            parse_grep_line(
+                "src/de lta.rs:pub fn delta<I>(lines: ByteLines<I>, writer: &mut dyn Write, config: &Config) -> std::io::Result<()>"
+            ),
+            Some(GrepLine {
+                path: "src/de lta.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "pub fn delta<I>(lines: ByteLines<I>, writer: &mut dyn Write, config: &Config) -> std::io::Result<()>".into(),
+                submatches: None,
+            })
+        );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+            parse_grep_line(
+                "src/de lta.rs:    pub fn new(writer: &'a mut dyn Write, config: &'a Config) -> Self {"
+            ),
+            Some(GrepLine {
+                path: "src/de lta.rs".into(),
+                line_number: None,
+                line_type: LineType::Match,
+                code: "    pub fn new(writer: &'a mut dyn Write, config: &'a Config) -> Self {".into(),
+                submatches: None,
+            })
+        );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_n_match() {
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/co-7-fig.rs:7:xxx"),
+                Some(GrepLine {
+                    path: "src/co-7-fig.rs".into(),
+                    line_number: Some(7),
+                    line_type: LineType::Match,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs:21:use crate::minusplus::MinusPlus;"),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: Some(21),
+                    line_type: LineType::Match,
+                    code: "use crate::minusplus::MinusPlus;".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line(
+                    "src/config.rs:95:    pub line_numbers_style_minusplus: MinusPlus<Style>,"
+                ),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: Some(95),
+                    line_type: LineType::Match,
+                    code: "    pub line_numbers_style_minusplus: MinusPlus<Style>,".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("Makefile:10:test: unit-test end-to-end-test"),
+                Some(GrepLine {
+                    path: "Makefile".into(),
+                    line_number: Some(10),
+                    line_type: LineType::Match,
+                    code: "test: unit-test end-to-end-test".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line(
+                    "Makefile:16:    ./tests/test_raw_output_matches_git_on_full_repo_history"
+                ),
+                Some(GrepLine {
+                    path: "Makefile".into(),
+                    line_number: Some(16),
+                    line_type: LineType::Match,
+                    code: "    ./tests/test_raw_output_matches_git_on_full_repo_history".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    #[ignore]
+    fn test_parse_grep_n_match_file_name_with_dashes_and_no_extension() {
+        // git grep -n
+        // This fails: we can't parse it currently.
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("etc/examples/119-within-line-edits:4:repo=$(mktemp -d)"),
+                Some(GrepLine {
+                    path: "etc/examples/119-within-line-edits".into(),
+                    line_number: Some(4),
+                    line_type: LineType::Match,
+                    code: "repo=$(mktemp -d)".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_no_match() {
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/co-7-fig.rs-xxx"),
+                Some(GrepLine {
+                    path: "src/co-7-fig.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Context,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs-    pub available_terminal_width: usize,"),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Context,
+                    code: "    pub available_terminal_width: usize,".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/con-fig.rs-use crate::minusplus::MinusPlus;"),
+                Some(GrepLine {
+                    path: "src/con-fig.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Context,
+                    code: "use crate::minusplus::MinusPlus;".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("de-lta.rs-            if self.source == Source::Unknown {"),
+                Some(GrepLine {
+                    path: "de-lta.rs".into(),
+                    line_number: None,
+                    line_type: LineType::Context,
+                    code: "            if self.source == Source::Unknown {".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_n_no_match() {
+        // git grep -n
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/co-7-fig.rs-7-xxx"),
+                Some(GrepLine {
+                    path: "src/co-7-fig.rs".into(),
+                    line_number: Some(7),
+                    line_type: LineType::Context,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs-58-    pub available_terminal_width: usize,"),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: Some(58),
+                    line_type: LineType::Context,
+                    code: "    pub available_terminal_width: usize,".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_match_no_extension() {
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("Makefile:xxx"),
+                Some(GrepLine {
+                    path: "Makefile".into(),
+                    line_number: None,
+                    line_type: LineType::Match,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_n_match_no_extension() {
+        // git grep -n
+        let fake_parent_grep_command =
+            "/usr/local/bin/grep --doesnt-matter --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("Makefile:7:xxx"),
+                Some(GrepLine {
+                    path: "Makefile".into(),
+                    line_number: Some(7),
+                    line_type: LineType::Match,
+                    code: "xxx".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    #[allow(non_snake_case)]
+    fn test_parse_grep_W_context_header() {
+        // git grep -W
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs=pub struct Config {"), // match
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: None,
+                    line_type: LineType::ContextHeader,
+                    code: "pub struct Config {".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    #[allow(non_snake_case)]
+    fn test_parse_grep_W_n_context_header() {
+        // git grep -n -W
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            assert_eq!(
+                parse_grep_line("src/config.rs=57=pub struct Config {"),
+                Some(GrepLine {
+                    path: "src/config.rs".into(),
+                    line_number: Some(57),
+                    line_type: LineType::ContextHeader,
+                    code: "pub struct Config {".into(),
+                    submatches: None,
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_not_grep_output() {
+        let fake_parent_grep_command =
+            "/usr/local/bin/git --doesnt-matter grep --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            let not_grep_output = "|       expose it in delta's color output styled with grep:";
+            assert_eq!(parse_grep_line(not_grep_output), None);
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_parent_command_is_not_grep_1() {
+        let fake_parent_grep_command =
+            "/usr/local/bin/notgrep --doesnt-matter --nor-this nor_this -- nor_this";
+        {
+            let _args = cfg::WithArgs::new(&fake_parent_grep_command);
+            let apparently_grep_output = "src/co-7-fig.rs:xxx";
+            assert_eq!(parse_grep_line(apparently_grep_output), None);
+        }
+    }
+
+    #[test]
+    fn test_parse_grep_parent_command_is_not_grep_2() {
+        {
+            // No fake parent grep command
+            let apparently_grep_output = "src/co-7-fig.rs:xxx";
+            assert_eq!(parse_grep_line(apparently_grep_output), None);
+        }
+    }
+}
diff --git a/src/handlers/hunk_header.rs b/src/handlers/hunk_header.rs
index 525b749..dd85150 100644
--- a/src/handlers/hunk_header.rs
+++ b/src/handlers/hunk_header.rs
@@ -27,8 +27,7 @@ use regex::Regex;
 use super::draw;
 use crate::config::Config;
 use crate::delta::{self, State, StateMachine};
-use crate::features;
-use crate::paint::{BgShouldFill, Painter};
+use crate::paint::{self, BgShouldFill, Painter, StyleSectionSpecifier};
 use crate::style::DecorationStyle;
 
 impl<'a> StateMachine<'a> {
@@ -155,7 +154,7 @@ fn write_hunk_header_raw(
     Ok(())
 }
 
-fn write_hunk_header(
+pub fn write_hunk_header(
     code_fragment: &str,
     line_numbers: &[(usize, usize)],
     painter: &mut Painter,
@@ -173,7 +172,9 @@ fn write_hunk_header(
         "".to_string()
     };
 
-    let file_with_line_number = get_painted_file_with_line_number(line_numbers, plus_file, config);
+    let plus_line_number = line_numbers[line_numbers.len() - 1].0;
+    let file_with_line_number =
+        paint_file_path_with_line_number(Some(plus_line_number), plus_file, config);
 
     if !line.is_empty() || !file_with_line_number.is_empty() {
         write_to_output_buffer(&file_with_line_number, line, painter, config);
@@ -191,41 +192,36 @@ fn write_hunk_header(
     Ok(())
 }
 
-fn get_painted_file_with_line_number(
-    line_numbers: &[(usize, usize)],
+fn paint_file_path_with_line_number(
+    line_number: Option<usize>,
     plus_file: &str,
     config: &Config,
 ) -> String {
-    let mut file_with_line_number = Vec::new();
-    let plus_line_number = line_numbers[line_numbers.len() - 1].0;
-    if config.hunk_header_style_include_file_path {
-        file_with_line_number.push(config.hunk_header_file_style.paint(plus_file))
+    let file_style = if config.hunk_header_style_include_file_path {
+        Some(config.hunk_header_file_style)
+    } else {
+        None
     };
-    if config.hunk_header_style_include_line_number
+    let line_number_style = if config.hunk_header_style_include_line_number
         && !config.hunk_header_style.is_raw
         && !config.color_only
+        && line_number.is_some()
     {
-        if !file_with_line_number.is_empty() {
-            file_with_line_number.push(ansi_term::ANSIString::from(":"));
-        }
-        file_with_line_number.push(
-            config
-                .hunk_header_line_number_style
-                .paint(format!("{}", plus_line_number)),
-        )
-    }
-    let file_with_line_number = ansi_term::ANSIStrings(&file_with_line_number).to_string();
-    if config.hyperlinks && !file_with_line_number.is_empty() {
-        features::hyperlinks::format_osc8_file_hyperlink(
-            plus_file,
-            Some(plus_line_number),
-            &file_with_line_number,
-            config,
-        )
-        .into()
+        Some(config.hunk_header_line_number_style)
     } else {
-        file_with_line_number
-    }
+        None
+    };
+
+    paint::paint_file_path_with_line_number(
+        line_number,
+        plus_file,
+        false,
+        ":",
+        false,
+        file_style,
+        line_number_style,
+        config,
+    )
 }
 
 fn write_to_output_buffer(
@@ -234,7 +230,7 @@ fn write_to_output_buffer(
     painter: &mut Painter,
     config: &Config,
 ) {
-    if config.navigate {
+    if !config.hunk_label.is_empty() {
         let _ = write!(
             &mut painter.output_buffer,
             "{} ",
@@ -254,7 +250,7 @@ fn write_to_output_buffer(
     if !line.is_empty() {
         painter.syntax_highlight_and_paint_line(
             &line,
-            config.hunk_header_style,
+            StyleSectionSpecifier::Style(config.hunk_header_style),
             delta::State::HunkHeader("".to_owned(), "".to_owned()),
             BgShouldFill::No,
         );
@@ -318,25 +314,25 @@ pub mod tests {
         assert_eq!(line_numbers_and_hunk_lengths[2], (358, 16),);
     }
     #[test]
-    fn test_get_painted_file_with_line_number_default() {
+    fn test_paint_file_path_with_line_number_default() {
         let cfg = integration_test_utils::make_config_from_args(&[]);
 
-        let result = get_painted_file_with_line_number(&vec![(3, 4)], "some-file", &cfg);
+        let result = paint_file_path_with_line_number(Some(3), "some-file", &cfg);
 
         assert_eq!(result, "\u{1b}[34m3\u{1b}[0m");
     }
 
     #[test]
-    fn test_get_painted_file_with_line_number_hyperlinks() {
+    fn test_paint_file_path_with_line_number_hyperlinks() {
         let cfg = integration_test_utils::make_config_from_args(&["--features", "hyperlinks"]);
 
-        let result = get_painted_file_with_line_number(&vec![(3, 4)], "some-file", &cfg);
+        let result = paint_file_path_with_line_number(Some(3), "some-file", &cfg);
 
         assert_eq!(result, "some-file");
     }
 
     #[test]
-    fn test_get_painted_file_with_line_number_empty() {
+    fn test_paint_file_path_with_line_number_empty() {
         let cfg = integration_test_utils::make_config_from_args(&[
             "--hunk-header-style",
             "syntax bold",
@@ -344,13 +340,13 @@ pub mod tests {
             "omit",
         ]);
 
-        let result = get_painted_file_with_line_number(&vec![(3, 4)], "some-file", &cfg);
+        let result = paint_file_path_with_line_number(Some(3), "some-file", &cfg);
 
         assert_eq!(result, "");
     }
 
     #[test]
-    fn test_get_painted_file_with_line_number_empty_hyperlinks() {
+    fn test_paint_file_path_with_line_number_empty_hyperlinks() {
         let cfg = integration_test_utils::make_config_from_args(&[
             "--hunk-header-style",
             "syntax bold",
@@ -360,13 +356,13 @@ pub mod tests {
             "hyperlinks",
         ]);
 
-        let result = get_painted_file_with_line_number(&vec![(3, 4)], "some-file", &cfg);
+        let result = paint_file_path_with_line_number(Some(3), "some-file", &cfg);
 
         assert_eq!(result, "");
     }
 
     #[test]
-    fn test_get_painted_file_with_line_number_empty_navigate() {
+    fn test_paint_file_path_with_line_number_empty_navigate() {
         let cfg = integration_test_utils::make_config_from_args(&[
             "--hunk-header-style",
             "syntax bold",
@@ -375,7 +371,7 @@ pub mod tests {
             "--navigate",
         ]);
 
-        let result = get_painted_file_with_line_number(&vec![(3, 4)], "δ some-file", &cfg);
+        let result = paint_file_path_with_line_number(Some(3), "δ some-file", &cfg);
 
         assert_eq!(result, "");
     }
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index db27e28..c032a7b 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -7,8 +7,11 @@ pub mod draw;
 pub mod file_meta;
 pub mod file_meta_diff;
 pub mod file_meta_misc;
+pub mod git_show_file;
+pub mod grep;
 pub mod hunk;
 pub mod hunk_header;
+mod ripgrep_json;
 pub mod submodule;
 
 use crate::delta::{State, StateMachine};
diff --git a/src/handlers/ripgrep_json.rs b/src/handlers/ripgrep_json.rs
new file mode 100644
index 0000000..1a278a1
--- /dev/null
+++ b/src/handlers/ripgrep_json.rs
@@ -0,0 +1,210 @@
+// See https://github.com/BurntSushi/ripgrep
+// This module implements handling of `rg --json` output. It is called by the
+// handler in handlers/grep.rs. Normal rg output (i.e. without --json) is
+// handled by the same code paths as `git grep` etc output, in handlers/grep.rs.
+use std::borrow::Cow;
+
+use crate::handlers::grep;
+use serde::Deserialize;
+use serde_json::Value;
+
+pub fn parse_line(line: &str) -> Option<grep::GrepLine> {
+    let ripgrep_line: Option<RipGrepLine> = serde_json::from_str(line).ok();
+    match ripgrep_line {
+        Some(ripgrep_line) => {
+            // A real line of rg --json output, i.e. either of type "match" or
+            // "context".
+            let mut code = ripgrep_line.data.lines.text;
+            if code.ends_with('\n') {
+                code.truncate(code.len() - 1);
+                if code.ends_with('\r') {
+                    code.truncate(code.len() - 1);
+                }
+            }
+            Some(grep::GrepLine {
+                line_type: ripgrep_line._type,
+                line_number: ripgrep_line.data.line_number,
+                path: Cow::from(ripgrep_line.data.path.text),
+                code: Cow::from(code),
+                submatches: Some(
+                    ripgrep_line
+                        .data
+                        .submatches
+                        .iter()
+                        .map(|m| (m.start, m.end))
+                        .collect(),
+                ),
+            })
+        }
+        None => {
+            let value: Value = serde_json::from_str(line).ok()?;
+            match &value["type"] {
+                Value::String(s) if s == "begin" || s == "end" || s == "summary" => {
+                    Some(grep::GrepLine {
+                        // ripgrep --json also emits these metadata lines at
+                        // file boundaries. We emit nothing but signal that the
+                        // line has been handled.
+                        line_type: grep::LineType::Ignore,
+                        line_number: None,
+                        path: "".into(),
+                        code: "".into(),
+                        submatches: None,
+                    })
+                }
+                _ => {
+                    // Failed to interpret the line as ripgrep output; allow
+                    // another delta handler to try.
+                    None
+                }
+            }
+        }
+    }
+}
+
+//   {
+//     "type": "match",
+//     "data": {
+//       "path": {
+//         "text": "src/cli.rs"
+//       },
+//       "lines": {
+//         "text": "    fn from_clap_and_git_config(\n"
+//       },
+//       "line_number": null,
+//       "absolute_offset": 35837,
+//       "submatches": [
+//         {
+//           "match": {
+//             "text": "fn"
+//           },
+//           "start": 4,
+//           "end": 6
+//         }
+//       ]
+//     }
+//   }
+
+#[derive(Deserialize, PartialEq, Debug)]
+struct RipGrepLine {
+    #[serde(rename(deserialize = "type"))]
+    _type: grep::LineType,
+    data: RipGrepLineData,
+}
+
+#[derive(Deserialize, PartialEq, Debug)]
+struct RipGrepLineData {
+    path: RipGrepLineText,
+    lines: RipGrepLineText,
+    line_number: Option<usize>,
+    absolute_offset: usize,
+    submatches: Vec<RipGrepLineSubmatch>,
+}
+
+#[derive(Deserialize, PartialEq, Debug)]
+struct RipGrepLineText {
+    text: String,
+}
+
+#[derive(Deserialize, PartialEq, Debug)]
+struct RipGrepLineSubmatch {
+    #[serde(rename(deserialize = "match"))]
+    _match: RipGrepLineText,
+    start: usize,
+    end: usize,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_deserialize() {
+        let line = r#"{"type":"match","data":{"path":{"text":"src/cli.rs"},"lines":{"text":"    fn from_clap_and_git_config(\n"},"line_number":null,"absolute_offset":35837,"submatches":[{"match":{"text":"fn"},"start":4,"end":6}]}}"#;
+        let ripgrep_line: RipGrepLine = serde_json::from_str(line).unwrap();
+        assert_eq!(
+            ripgrep_line,
+            RipGrepLine {
+                _type: grep::LineType::Match,
+                data: RipGrepLineData {
+                    path: RipGrepLineText {
+                        text: "src/cli.rs".into()
+                    },
+                    lines: RipGrepLineText {
+                        text: "    fn from_clap_and_git_config(\n".into(),
+                    },
+                    line_number: None,
+                    absolute_offset: 35837,
+                    submatches: vec![RipGrepLineSubmatch {
+                        _match: RipGrepLineText { text: "fn".into() },
+                        start: 4,
+                        end: 6
+                    }]
+                }
+            }
+        )
+    }
+
+    #[test]
+    fn test_deserialize_2() {
+        let line = r#"{"type":"match","data":{"path":{"text":"src/handlers/submodule.rs"},"lines":{"text":"                        .paint(minus_commit.chars().take(7).collect::<String>()),\n"},"line_number":41,"absolute_offset":1430,"submatches":[{"match":{"text":"("},"start":30,"end":31},{"match":{"text":"("},"start":49,"end":50},{"match":{"text":")"},"start":50,"end":51},{"match":{"text":"("},"start":56,"end":57},{"match":{"text":")"},"start":58,"end":59},{"match":{"text":"("},"start":77,"end":78},{"match":{"text":")"},"start":78,"end":79},{"match":{"text":")"},"start":79,"end":80}]}}"#;
+        let ripgrep_line: RipGrepLine = serde_json::from_str(line).unwrap();
+        assert_eq!(
+            ripgrep_line,
+            RipGrepLine {
+                _type: grep::LineType::Match,
+                data: RipGrepLineData {
+                    path: RipGrepLineText {
+                        text: "src/handlers/submodule.rs".into()
+                    },
+                    lines: RipGrepLineText {
+                        text: "                        .paint(minus_commit.chars().take(7).collect::<String>()),\n".into(),
+                    },
+                    line_number: Some(41),
+                    absolute_offset: 1430,
+                    submatches: vec![
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: "(".into() },
+                            start: 30,
+                            end: 31
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: "(".into() },
+                            start: 49,
+                            end: 50
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: ")".into() },
+                            start: 50,
+                            end: 51
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: "(".into() },
+                            start: 56,
+                            end: 57
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: ")".into() },
+                            start: 58,
+                            end: 59
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: "(".into() },
+                            start: 77,
+                            end: 78
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: ")".into() },
+                            start: 78,
+                            end: 79
+                        },
+                        RipGrepLineSubmatch {
+                            _match: RipGrepLineText { text: ")".into() },
+                            start: 79,
+                            end: 80
+                        },
+                    ]
+                }
+            }
+        )
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 6a6e3c7..19d0d26 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,6 +7,7 @@ mod align;
 mod ansi;
 mod cli;
 mod color;
+mod colors;
 mod config;
 mod delta;
 mod edits;
@@ -19,6 +20,7 @@ mod minusplus;
 mod options;
 mod paint;
 mod parse_style;
+mod parse_styles;
 mod style;
 mod utils;
 mod wrapping;
@@ -80,19 +82,32 @@ fn run_app() -> std::io::Result<i32> {
     let assets = HighlightingAssets::new();
     let opt = cli::Opt::from_args_and_git_config(git_config::GitConfig::try_create(), assets);
 
-    if opt.list_languages {
-        list_languages()?;
-        return Ok(0);
+    let subcommand_result = if opt.list_languages {
+        Some(list_languages())
     } else if opt.list_syntax_themes {
-        subcommands::list_syntax_themes::list_syntax_themes()?;
-        return Ok(0);
+        Some(subcommands::list_syntax_themes::list_syntax_themes())
     } else if opt.show_syntax_themes {
-        subcommands::show_syntax_themes::show_syntax_themes()?;
-        return Ok(0);
+        Some(subcommands::show_syntax_themes::show_syntax_themes())
     } else if opt.show_themes {
-        subcommands::show_themes::show_themes(opt.dark, opt.light, opt.computed.is_light_mode)?;
+        Some(subcommands::show_themes::show_themes(
+            opt.dark,
+            opt.light,
+            opt.computed.is_light_mode,
+        ))
+    } else if opt.show_colors {
+        Some(subcommands::show_colors::show_colors())
+    } else {
+        None
+    };
+    if let Some(result) = subcommand_result {
+        if let Err(error) = result {
+            match error.kind() {
+                ErrorKind::BrokenPipe => {}
+                _ => fatal(format!("{}", error)),
+            }
+        }
         return Ok(0);
-    }
+    };
 
     let _show_config = opt.show_config;
     let config = config::Config::from(opt);
diff --git a/src/options/set.rs b/src/options/set.rs
index 125d450..b1dd49d 100644
--- a/src/options/set.rs
+++ b/src/options/set.rs
@@ -140,6 +140,12 @@ pub fn set_options(
             right_arrow,
             hunk_label,
             file_style,
+            grep_context_line_style,
+            grep_file_style,
+            grep_line_number_style,
+            grep_match_line_style,
+            grep_match_word_style,
+            grep_separator_symbol,
             hunk_header_decoration_style,
             hunk_header_file_style,
             hunk_header_line_number_style,
@@ -161,6 +167,7 @@ pub fn set_options(
             minus_non_emph_style,
             minus_non_emph_style,
             navigate,
+            navigate_regex,
             line_fill_method,
             line_numbers,
             line_numbers_left_format,
@@ -180,6 +187,7 @@ pub fn set_options(
             plus_non_emph_style,
             raw,
             relative_paths,
+            show_colors,
             show_themes,
             side_by_side,
             wrap_max_lines,
@@ -689,6 +697,7 @@ pub mod tests {
     minus-non-emph-style = black black
     minus-style = black black
     navigate = true
+    navigate-regex = xxxyyyzzz
     paging = never
     plus-emph-style = black black
     plus-empty-line-marker-style = black black
@@ -749,6 +758,7 @@ pub mod tests {
         assert_eq!(opt.minus_non_emph_style, "black black");
         assert_eq!(opt.minus_style, "black black");
         assert_eq!(opt.navigate, true);
+        assert_eq!(opt.navigate_regex, Some("xxxyyyzzz".to_string()));
         assert_eq!(opt.paging_mode, "never");
         assert_eq!(opt.plus_emph_style, "black black");
         assert_eq!(opt.plus_empty_line_marker_style, "black black");
diff --git a/src/paint.rs b/src/paint.rs
index ecf76e2..529065f 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -7,9 +7,10 @@ use syntect::parsing::{SyntaxReference, SyntaxSet};
 use unicode_segmentation::UnicodeSegmentation;
 
 use crate::ansi;
-use crate::config::{self, delta_unreachable};
+use crate::config::{self, delta_unreachable, Config};
 use crate::delta::State;
 use crate::edits;
+use crate::features::hyperlinks;
 use crate::features::line_numbers;
 use crate::features::side_by_side::ansifill;
 use crate::features::side_by_side::{self, available_line_width, LineSegments, PanelSide};
@@ -55,6 +56,12 @@ impl Default for BgShouldFill {
     }
 }
 
+#[derive(PartialEq, Debug)]
+pub enum StyleSectionSpecifier<'l> {
+    Style(Style),
+    StyleSections(LineSegments<'l, Style>),
+}
+
 impl<'p> Painter<'p> {
     pub fn new(writer: &'p mut dyn Write, config: &'p config::Config) -> Self {
         let default_syntax = Self::get_syntax(&config.syntax_set, None);
@@ -406,10 +413,13 @@ impl<'p> Painter<'p> {
     }
 
     /// Write painted line to the output buffer, with syntax-highlighting and `style` superimposed.
+    // Note that, if passing `style_sections` as
+    // `StyleSectionSpecifier::StyleSections`, then tabs must already have been
+    // expanded in the text.
     pub fn syntax_highlight_and_paint_line(
         &mut self,
         line: &str,
-        style: Style,
+        style_sections: StyleSectionSpecifier,
         state: State,
         background_color_extends_to_terminal_width: BgShouldFill,
     ) {
@@ -420,7 +430,10 @@ impl<'p> Painter<'p> {
             self.highlighter.as_mut(),
             self.config,
         );
-        let diff_style_sections = vec![vec![(style, lines[0].0.as_str())]]; // TODO: compute style from state
+        let diff_style_sections = match style_sections {
+            StyleSectionSpecifier::Style(style) => vec![vec![(style, lines[0].0.as_str())]],
+            StyleSectionSpecifier::StyleSections(style_sections) => vec![style_sections],
+        };
         Painter::paint_lines(
             syntax_style_sections,
             diff_style_sections,
@@ -623,10 +636,21 @@ impl<'p> Painter<'p> {
             State::HunkHeader(_, _) => true,
             State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => false,
             State::Blame(_, _) => true,
-            _ => panic!(
-                "should_compute_syntax_highlighting is undefined for state {:?}",
-                state
-            ),
+            State::GitShowFile => true,
+            State::Grep => true,
+            State::Unknown
+            | State::CommitMeta
+            | State::FileMeta
+            | State::HunkMinusWrapped
+            | State::HunkZeroWrapped
+            | State::HunkPlusWrapped
+            | State::SubmoduleLog
+            | State::SubmoduleShort(_) => {
+                panic!(
+                    "should_compute_syntax_highlighting is undefined for state {:?}",
+                    state
+                )
+            }
         }
     }
 
@@ -691,14 +715,15 @@ impl<'p> Painter<'p> {
         } else {
             None
         };
-        Self::update_styles(&mut diff_sections.0, None, minus_non_emph_style);
+        let mut lines_style_sections = MinusPlus::new(&mut diff_sections.0, &mut diff_sections.1);
+        Self::update_styles(lines_style_sections[Minus], None, minus_non_emph_style);
         let plus_non_emph_style = if config.plus_non_emph_style != config.plus_emph_style {
             Some(config.plus_non_emph_style)
         } else {
             None
         };
         Self::update_styles(
-            &mut diff_sections.1,
+            lines_style_sections[Plus],
             Some(config.whitespace_error_style),
             plus_non_emph_style,
         );
@@ -714,35 +739,89 @@ impl<'p> Painter<'p> {
     /// 2. If the line constitutes a whitespace error, then the whitespace error style
     ///    should be applied to the added material.
     fn update_styles(
-        style_sections: &mut Vec<LineSegments<'_, Style>>,
+        lines_style_sections: &mut Vec<LineSegments<'_, Style>>,
         whitespace_error_style: Option<Style>,
         non_emph_style: Option<Style>,
     ) {
-        for line_sections in style_sections {
+        for style_sections in lines_style_sections {
             let line_has_emph_and_non_emph_sections =
-                style_sections_contain_more_than_one_style(line_sections);
+                style_sections_contain_more_than_one_style(style_sections);
             let should_update_non_emph_styles =
                 non_emph_style.is_some() && line_has_emph_and_non_emph_sections;
             let is_whitespace_error =
-                whitespace_error_style.is_some() && is_whitespace_error(line_sections);
-            for section in line_sections.iter_mut() {
+                whitespace_error_style.is_some() && is_whitespace_error(style_sections);
+            for (style, _) in style_sections.iter_mut() {
                 // If the line as a whole constitutes a whitespace error then highlight this
                 // section if either (a) it is an emph section, or (b) the line lacks any
                 // emph/non-emph distinction.
-                if is_whitespace_error
-                    && (section.0.is_emph || !line_has_emph_and_non_emph_sections)
-                {
-                    *section = (whitespace_error_style.unwrap(), section.1);
+                if is_whitespace_error && (style.is_emph || !line_has_emph_and_non_emph_sections) {
+                    *style = whitespace_error_style.unwrap();
                 }
                 // Otherwise, update the style if this is a non-emph section that needs updating.
-                else if should_update_non_emph_styles && !section.0.is_emph {
-                    *section = (non_emph_style.unwrap(), section.1);
+                else if should_update_non_emph_styles && !style.is_emph {
+                    *style = non_emph_style.unwrap();
                 }
             }
         }
     }
 }
 
+#[allow(clippy::too_many_arguments)]
+pub fn paint_file_path_with_line_number(
+    line_number: Option<usize>,
+    plus_file: &str,
+    pad_line_number: bool,
+    separator: &str,
+    terminate_with_separator: bool,
+    file_style: Option<Style>,        // None means do not include file path
+    line_number_style: Option<Style>, // None means do not include line number
+    config: &Config,
+) -> String {
+    let mut file_with_line_number = Vec::new();
+    if let Some(file_style) = file_style {
+        file_with_line_number.push(file_style.paint(plus_file))
+    };
+    if let Some(line_number) = line_number {
+        if let Some(line_number_style) = line_number_style {
+            if !file_with_line_number.is_empty() {
+                file_with_line_number.push(ansi_term::ANSIString::from(separator));
+            }
+            file_with_line_number.push(line_number_style.paint(format!("{}", line_number)))
+        }
+    }
+    if terminate_with_separator {
+        file_with_line_number.push(ansi_term::ANSIGenericString::from(separator));
+    }
+    if pad_line_number {
+        // If requested we pad line numbers to a width of at least
+        // 3, so that we do not see any misalignment up to line
+        // number 999. However, see
+        // https://github.com/BurntSushi/ripgrep/issues/795 for
+        // discussion about aligning grep output.
+        match line_number {
+            Some(n) if n < 10 => {
+                file_with_line_number.push(ansi_term::ANSIGenericString::from("  "))
+            }
+            Some(n) if n < 100 => {
+                file_with_line_number.push(ansi_term::ANSIGenericString::from(" "))
+            }
+            _ => {}
+        }
+    }
+    let file_with_line_number = ansi_term::ANSIStrings(&file_with_line_number).to_string();
+    if config.hyperlinks && !file_with_line_number.is_empty() {
+        hyperlinks::format_osc8_file_hyperlink(
+            plus_file,
+            line_number,
+            &file_with_line_number,
+            config,
+        )
+        .into()
+    } else {
+        file_with_line_number
+    }
+}
+
 // edits::annotate doesn't return "coalesced" annotations (see comment there), so we can't assume
 // that `sections.len() > 1 <=> (multiple styles)`.
 fn style_sections_contain_more_than_one_style(sections: &[(Style, &str)]) -> bool {
diff --git a/src/parse_style.rs b/src/parse_style.rs
index 820cc03..7c9ce34 100644
--- a/src/parse_style.rs
+++ b/src/parse_style.rs
@@ -3,6 +3,7 @@ use bitflags::bitflags;
 use crate::color;
 use crate::config::delta_unreachable;
 use crate::fatal;
+use crate::git_config::GitConfig;
 use crate::style::{DecorationStyle, Style};
 
 impl Style {
@@ -15,15 +16,18 @@ impl Style {
         default: Option<Self>,
         decoration_style_string: Option<&str>,
         true_color: bool,
-        is_emph: bool,
+        git_config: Option<&GitConfig>,
     ) -> Self {
         let (ansi_term_style, is_omitted, is_raw, is_syntax_highlighted) =
-            parse_ansi_term_style(style_string, default, true_color);
-        let decoration_style =
-            DecorationStyle::from_str(decoration_style_string.unwrap_or(""), true_color);
+            parse_ansi_term_style(style_string, default, true_color, git_config);
+        let decoration_style = DecorationStyle::from_str(
+            decoration_style_string.unwrap_or(""),
+            true_color,
+            git_config,
+        );
         Self {
             ansi_term_style,
-            is_emph,
+            is_emph: false,
             is_omitted,
             is_raw,
             is_syntax_highlighted,
@@ -32,7 +36,7 @@ impl Style {
     }
 
     pub fn from_git_str(git_style_string: &str) -> Self {
-        Self::from_str(git_style_string, None, None, true, false)
+        Self::from_str(git_style_string, None, None, true, None)
     }
 
     /// Construct Style but interpreting 'ul', 'box', etc as applying to the decoration style.
@@ -41,7 +45,7 @@ impl Style {
         default: Option<Self>,
         decoration_style_string: Option<&str>,
         true_color: bool,
-        is_emph: bool,
+        git_config: Option<&GitConfig>,
     ) -> Self {
         let (special_attributes_from_style_string, style_string) =
             extract_special_decoration_attributes_from_non_decoration_style_string(style_string);
@@ -50,7 +54,7 @@ impl Style {
             default,
             decoration_style_string,
             true_color,
-            is_emph,
+            git_config,
         );
         // TODO: box in this context resulted in box-with-underline for commit and file
         style.decoration_style = DecorationStyle::apply_special_decoration_attributes(
@@ -68,20 +72,22 @@ impl Style {
         decoration_style_string: Option<&str>,
         deprecated_foreground_color_arg: Option<&str>,
         true_color: bool,
-        is_emph: bool,
+        git_config: Option<&GitConfig>,
     ) -> Self {
         let mut style = Self::from_str_with_handling_of_special_decoration_attributes(
             style_string,
             default,
             decoration_style_string,
             true_color,
-            is_emph,
+            git_config,
         );
         if let Some(s) = deprecated_foreground_color_arg {
             // The deprecated --{commit,file,hunk}-color args functioned to set the decoration
             // foreground color. In the case of file, it set the text foreground color also.
             let foreground_from_deprecated_arg =
-                parse_ansi_term_style(s, None, true_color).0.foreground;
+                parse_ansi_term_style(s, None, true_color, git_config)
+                    .0
+                    .foreground;
             style.ansi_term_style.foreground = foreground_from_deprecated_arg;
             style.decoration_style = match style.decoration_style {
                 DecorationStyle::Box(mut ansi_term_style) => {
@@ -129,11 +135,11 @@ bitflags! {
 }
 
 impl DecorationStyle {
-    pub fn from_str(style_string: &str, true_color: bool) -> Self {
+    pub fn from_str(style_string: &str, true_color: bool, git_config: Option<&GitConfig>) -> Self {
         let (special_attributes, style_string) =
             extract_special_decoration_attributes(style_string);
         let (style, is_omitted, is_raw, is_syntax_highlighted) =
-            parse_ansi_term_style(&style_string, None, true_color);
+            parse_ansi_term_style(&style_string, None, true_color, git_config);
         if is_raw {
             fatal("'raw' may not be used in a decoration style.");
         };
@@ -200,6 +206,7 @@ fn parse_ansi_term_style(
     s: &str,
     default: Option<Style>,
     true_color: bool,
+    git_config: Option<&GitConfig>,
 ) -> (ansi_term::Style, bool, bool, bool) {
     let mut style = ansi_term::Style::new();
     let mut seen_foreground = false;
@@ -248,7 +255,7 @@ fn parse_ansi_term_style(
                 style.foreground = default.and_then(|s| s.ansi_term_style.foreground);
                 is_syntax_highlighted = default.map(|s| s.is_syntax_highlighted).unwrap_or(false);
             } else {
-                style.foreground = color::parse_color(word, true_color);
+                style.foreground = color::parse_color(word, true_color, git_config);
             }
             seen_foreground = true;
         } else if !seen_background {
@@ -262,7 +269,7 @@ fn parse_ansi_term_style(
                 background_is_auto = true;
                 style.background = default.and_then(|s| s.ansi_term_style.background);
             } else {
-                style.background = color::parse_color(word, true_color);
+                style.background = color::parse_color(word, true_color, git_config);
             }
             seen_background = true;
         } else {
@@ -330,11 +337,11 @@ mod tests {
     #[test]
     fn test_parse_ansi_term_style() {
         assert_eq!(
-            parse_ansi_term_style("", None, false),
+            parse_ansi_term_style("", None, false, None),
             (ansi_term::Style::new(), false, false, false)
         );
         assert_eq!(
-            parse_ansi_term_style("red", None, false),
+            parse_ansi_term_style("red", None, false, None),
             (
                 ansi_term::Style {
                     foreground: Some(ansi_term::Color::Red),
@@ -346,7 +353,7 @@ mod tests {
             )
         );
         assert_eq!(
-            parse_ansi_term_style("red green", None, false),
+            parse_ansi_term_style("red green", None, false, None),
             (
                 ansi_term::Style {
                     foreground: Some(ansi_term::Color::Red),
@@ -359,7 +366,7 @@ mod tests {
             )
         );
         assert_eq!(
-            parse_ansi_term_style("bold red underline green blink", None, false),
+            parse_ansi_term_style("bold red underline green blink", None, false, None),
             (
                 ansi_term::Style {
                     foreground: Some(ansi_term::Color::Red),
@@ -379,11 +386,11 @@ mod tests {
     #[test]
     fn test_parse_ansi_term_style_with_special_syntax_color() {
         assert_eq!(
-            parse_ansi_term_style("syntax", None, false),
+            parse_ansi_term_style("syntax", None, false, None),
             (ansi_term::Style::new(), false, false, true)
         );
         assert_eq!(
-            parse_ansi_term_style("syntax italic white hidden", None, false),
+            parse_ansi_term_style("syntax italic white hidden", None, false, None),
             (
                 ansi_term::Style {
                     background: Some(ansi_term::Color::White),
@@ -397,7 +404,7 @@ mod tests {
             )
         );
         assert_eq!(
-            parse_ansi_term_style("bold syntax italic white hidden", None, false),
+            parse_ansi_term_style("bold syntax italic white hidden", None, false, None),
             (
                 ansi_term::Style {
                     background: Some(ansi_term::Color::White),
@@ -416,12 +423,12 @@ mod tests {
     #[test]
     fn test_parse_ansi_term_style_with_special_omit_attribute() {
         assert_eq!(
-            parse_ansi_term_style("omit", None, false),
+            parse_ansi_term_style("omit", None, false, None),
             (ansi_term::Style::new(), true, false, false)
         );
         // It doesn't make sense for omit to be combined with anything else, but it is not an error.
         assert_eq!(
-            parse_ansi_term_style("omit syntax italic white hidden", None, false),
+            parse_ansi_term_style("omit syntax italic white hidden", None, false, None),
             (
                 ansi_term::Style {
                     background: Some(ansi_term::Color::White),
@@ -439,12 +446,12 @@ mod tests {
     #[test]
     fn test_parse_ansi_term_style_with_special_raw_attribute() {
         assert_eq!(
-            parse_ansi_term_style("raw", None, false),
+            parse_ansi_term_style("raw", None, false, None),
             (ansi_term::Style::new(), false, true, false)
         );
         // It doesn't make sense for raw to be combined with anything else, but it is not an error.
         assert_eq!(
-            parse_ansi_term_style("raw syntax italic white hidden", None, false),
+            parse_ansi_term_style("raw syntax italic white hidden", None, false, None),
             (
                 ansi_term::Style {
                     background: Some(ansi_term::Color::White),
@@ -505,7 +512,7 @@ mod tests {
     #[test]
     fn test_decoration_style_from_str_empty_string() {
         assert_eq!(
-            DecorationStyle::from_str("", true),
+            DecorationStyle::from_str("", true, None),
             DecorationStyle::NoDecoration,
         )
     }
@@ -513,7 +520,7 @@ mod tests {
     #[test]
     fn test_decoration_style_from_str() {
         assert_eq!(
-            DecorationStyle::from_str("ol red box bold green ul", true),
+            DecorationStyle::from_str("ol red box bold green ul", true, None),
             DecorationStyle::BoxWithUnderOverline(ansi_term::Style {
                 foreground: Some(ansi_term::Color::Red),
                 background: Some(ansi_term::Color::Green),
@@ -530,7 +537,7 @@ mod tests {
             None,
             Some("ol red box bold green ul"),
             true,
-            false,
+            None,
         );
         let red_green_bold = ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
@@ -550,7 +557,7 @@ mod tests {
 
     #[test]
     fn test_style_from_str_raw_with_box() {
-        let actual_style = Style::from_str("raw", None, Some("box"), true, false);
+        let actual_style = Style::from_str("raw", None, Some("box"), true, None);
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
             actual_style,
@@ -565,7 +572,7 @@ mod tests {
 
     #[test]
     fn test_style_from_str_decoration_style_only() {
-        let actual_style = Style::from_str("", None, Some("ol red box bold green ul"), true, false);
+        let actual_style = Style::from_str("", None, Some("ol red box bold green ul"), true, None);
         let red_green_bold = ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
             background: Some(ansi_term::Color::Green),
@@ -588,7 +595,7 @@ mod tests {
             None,
             Some("ol red box bold green ul"),
             true,
-            false,
+            None,
         );
         let expected_decoration_style = DecorationStyle::BoxWithUnderOverline(ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
@@ -612,7 +619,7 @@ mod tests {
             None,
             Some("box"),
             true,
-            false,
+            None,
         );
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
@@ -636,7 +643,7 @@ mod tests {
             ..ansi_term::Style::new()
         });
         let actual_style = Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-                "", None, Some("ol red box bold green ul"), None, true, false
+                "", None, Some("ol red box bold green ul"), None, true, None
             );
         assert_eq!(
             actual_style,
@@ -656,7 +663,7 @@ mod tests {
             Some("box"),
             None,
             true,
-            false,
+            None,
         );
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
diff --git a/src/parse_styles.rs b/src/parse_styles.rs
new file mode 100644
index 0000000..39e436f
--- /dev/null
+++ b/src/parse_styles.rs
@@ -0,0 +1,578 @@
+use std::collections::{HashMap, HashSet};
+
+use crate::cli;
+use crate::color;
+use crate::fatal;
+use crate::git_config::{GitConfig, GitConfigEntry};
+use crate::style::{self, Style};
+
+#[derive(Debug, Clone)]
+enum StyleReference {
+    Style(Style),
+    Reference(String),
+}
+
+fn is_style_reference(style_string: &str) -> bool {
+    style_string.ends_with("-style") && !style_string.chars().any(|c| c == ' ')
+}
+
+pub fn parse_styles(opt: &cli::Opt) -> HashMap<String, Style> {
+    let mut styles: HashMap<&str, StyleReference> = HashMap::new();
+
+    make_hunk_styles(opt, &mut styles);
+    make_commit_file_hunk_header_styles(opt, &mut styles);
+    make_line_number_styles(opt, &mut styles);
+    make_grep_styles(opt, &mut styles);
+
+    styles.insert(
+        "inline-hint-style",
+        style_from_str(
+            &opt.inline_hint_style,
+            None,
+            None,
+            opt.computed.true_color,
+            opt.git_config.as_ref(),
+        ),
+    );
+    styles.insert(
+        "git-minus-style",
+        StyleReference::Style(match opt.git_config_entries.get("color.diff.old") {
+            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
+            _ => *style::GIT_DEFAULT_MINUS_STYLE,
+        }),
+    );
+    styles.insert(
+        "git-plus-style",
+        StyleReference::Style(match opt.git_config_entries.get("color.diff.new") {
+            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
+            _ => *style::GIT_DEFAULT_PLUS_STYLE,
+        }),
+    );
+    let mut resolved_styles = resolve_style_references(styles, opt);
+    resolved_styles.get_mut("minus-emph-style").unwrap().is_emph = true;
+    resolved_styles.get_mut("plus-emph-style").unwrap().is_emph = true;
+    resolved_styles
+}
+
+fn resolve_style_references(
+    edges: HashMap<&str, StyleReference>,
+    opt: &cli::Opt,
+) -> HashMap<String, Style> {
+    let mut resolved_styles = HashMap::new();
+
+    for starting_node in edges.keys() {
+        if resolved_styles.contains_key(*starting_node) {
+            continue;
+        }
+        let mut visited = HashSet::new();
+        let mut node = *starting_node;
+        loop {
+            if !visited.insert(node) {
+                #[cfg(not(test))]
+                fatal(format!("Your delta styles form a cycle! {:?}", visited));
+                #[cfg(test)]
+                return [("__cycle__", Style::default())]
+                    .iter()
+                    .map(|(a, b)| (a.to_string(), *b))
+                    .collect();
+            }
+            match &edges.get(&node) {
+                Some(StyleReference::Reference(child_node)) => node = child_node,
+                Some(StyleReference::Style(style)) => {
+                    resolved_styles.extend(visited.iter().map(|node| (node.to_string(), *style)));
+                    break;
+                }
+                None => {
+                    if let Some(git_config) = &opt.git_config {
+                        match git_config.get::<String>(&format!("delta.{}", node)) {
+                            Some(s) => {
+                                let style = Style::from_git_str(&s);
+                                resolved_styles
+                                    .extend(visited.iter().map(|node| (node.to_string(), style)));
+                                break;
+                            }
+                            _ => fatal(format!("Style not found: {}", node)),
+                        }
+                    } else {
+                        fatal(format!("Style not found: {}", node));
+                    }
+                }
+            }
+        }
+    }
+    resolved_styles
+}
+
+fn make_hunk_styles<'a>(opt: &'a cli::Opt, styles: &'a mut HashMap<&str, StyleReference>) {
+    let is_light_mode = opt.computed.is_light_mode;
+    let true_color = opt.computed.true_color;
+    let minus_style = style_from_str(
+        &opt.minus_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let minus_emph_style = style_from_str(
+        &opt.minus_emph_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_emph_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let minus_non_emph_style = style_from_str(
+        &opt.minus_non_emph_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    // The style used to highlight a removed empty line when otherwise it would be invisible due to
+    // lack of background color in minus-style.
+    let minus_empty_line_marker_style = style_from_str(
+        &opt.minus_empty_line_marker_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let zero_style = style_from_str(
+        &opt.zero_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let plus_style = style_from_str(
+        &opt.plus_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let plus_emph_style = style_from_str(
+        &opt.plus_emph_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_emph_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let plus_non_emph_style = style_from_str(
+        &opt.plus_non_emph_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    // The style used to highlight an added empty line when otherwise it would be invisible due to
+    // lack of background color in plus-style.
+    let plus_empty_line_marker_style = style_from_str(
+        &opt.plus_empty_line_marker_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let whitespace_error_style = style_from_str(
+        &opt.whitespace_error_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    styles.extend([
+        ("minus-style", minus_style),
+        ("minus-emph-style", minus_emph_style),
+        ("minus-non-emph-style", minus_non_emph_style),
+        (
+            "minus-empty-line-marker-style",
+            minus_empty_line_marker_style,
+        ),
+        ("zero-style", zero_style),
+        ("plus-style", plus_style),
+        ("plus-emph-style", plus_emph_style),
+        ("plus-non-emph-style", plus_non_emph_style),
+        ("plus-empty-line-marker-style", plus_empty_line_marker_style),
+        ("whitespace-error-style", whitespace_error_style),
+    ])
+}
+
+fn make_line_number_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) {
+    let true_color = opt.computed.true_color;
+    let line_numbers_left_style = style_from_str(
+        &opt.line_numbers_left_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let line_numbers_minus_style = style_from_str(
+        &opt.line_numbers_minus_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let line_numbers_zero_style = style_from_str(
+        &opt.line_numbers_zero_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let line_numbers_plus_style = style_from_str(
+        &opt.line_numbers_plus_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    let line_numbers_right_style = style_from_str(
+        &opt.line_numbers_right_style,
+        None,
+        None,
+        true_color,
+        opt.git_config.as_ref(),
+    );
+
+    styles.extend([
+        ("line-numbers-minus-style", line_numbers_minus_style),
+        ("line-numbers-zero-style", line_numbers_zero_style),
+        ("line-numbers-plus-style", line_numbers_plus_style),
+        ("line-numbers-left-style", line_numbers_left_style),
+        ("line-numbers-right-style", line_numbers_right_style),
+    ])
+}
+
+fn make_commit_file_hunk_header_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) {
+    let true_color = opt.computed.true_color;
+    styles.extend([
+        ("commit-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.commit_style,
+                None,
+                Some(&opt.commit_decoration_style),
+                opt.deprecated_commit_color.as_deref(),
+                true_color,
+                opt.git_config.as_ref(),
+            )
+        ),
+        ("file-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.file_style,
+                None,
+                Some(&opt.file_decoration_style),
+                opt.deprecated_file_color.as_deref(),
+                true_color,
+                opt.git_config.as_ref(),
+            )
+        ),
+        ("hunk-header-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.hunk_header_style,
+                None,
+                Some(&opt.hunk_header_decoration_style),
+                opt.deprecated_hunk_color.as_deref(),
+                true_color,
+                opt.git_config.as_ref(),
+            )
+        ),
+        ("hunk-header-file-style",
+            style_from_str_with_handling_of_special_decoration_attributes(
+                &opt.hunk_header_file_style,
+                None,
+                None,
+                true_color,
+                opt.git_config.as_ref(),
+            )
+        ),
+        ("hunk-header-line-number-style",
+            style_from_str_with_handling_of_special_decoration_attributes(
+                &opt.hunk_header_line_number_style,
+                None,
+                None,
+                true_color,
+                opt.git_config.as_ref(),
+            )
+        ),
+    ]);
+}
+
+fn make_grep_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) {
+    styles.extend([
+        (
+            "grep-match-line-style",
+            if let Some(s) = &opt.grep_match_line_style {
+                style_from_str(
+                    s,
+                    None,
+                    None,
+                    opt.computed.true_color,
+                    opt.git_config.as_ref(),
+                )
+            } else {
+                StyleReference::Reference("zero-style".to_owned())
+            },
+        ),
+        (
+            "grep-match-word-style",
+            if let Some(s) = &opt.grep_match_word_style {
+                style_from_str(
+                    s,
+                    None,
+                    None,
+                    opt.computed.true_color,
+                    opt.git_config.as_ref(),
+                )
+            } else {
+                StyleReference::Reference("plus-emph-style".to_owned())
+            },
+        ),
+        (
+            "grep-context-line-style",
+            if let Some(s) = &opt.grep_context_line_style {
+                style_from_str(
+                    s,
+                    None,
+                    None,
+                    opt.computed.true_color,
+                    opt.git_config.as_ref(),
+                )
+            } else {
+                StyleReference::Reference("zero-style".to_owned())
+            },
+        ),
+        (
+            "grep-file-style",
+            if let Some(s) = &opt.grep_file_style {
+                style_from_str(
+                    s,
+                    None,
+                    None,
+                    opt.computed.true_color,
+                    opt.git_config.as_ref(),
+                )
+            } else {
+                StyleReference::Reference("hunk-header-file-style".to_owned())
+            },
+        ),
+        (
+            "grep-line-number-style",
+            if let Some(s) = &opt.grep_line_number_style {
+                style_from_str(
+                    s,
+                    None,
+                    None,
+                    opt.computed.true_color,
+                    opt.git_config.as_ref(),
+                )
+            } else {
+                StyleReference::Reference("hunk-header-line-number-style".to_owned())
+            },
+        ),
+    ])
+}
+
+fn style_from_str(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    true_color: bool,
+    git_config: Option<&GitConfig>,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(Style::from_str(
+            style_string,
+            default,
+            decoration_style_string,
+            true_color,
+            git_config,
+        ))
+    }
+}
+
+fn style_from_str_with_handling_of_special_decoration_attributes(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    true_color: bool,
+    git_config: Option<&GitConfig>,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(
+            Style::from_str_with_handling_of_special_decoration_attributes(
+                style_string,
+                default,
+                decoration_style_string,
+                true_color,
+                git_config,
+            ),
+        )
+    }
+}
+
+fn style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    deprecated_foreground_color_arg: Option<&str>,
+    true_color: bool,
+    git_config: Option<&GitConfig>,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+            style_string,
+            default,
+            decoration_style_string,
+            deprecated_foreground_color_arg,
+            true_color,
+            git_config,
+        ))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::tests::integration_test_utils;
+
+    fn resolve_style_references(edges: HashMap<&str, StyleReference>) -> HashMap<String, Style> {
+        let opt = integration_test_utils::make_options_from_args(&[]);
+        super::resolve_style_references(edges, &opt)
+    }
+
+    #[test]
+    fn test_resolve_style_references_1() {
+        let style_1 = Style::default();
+        let mut style_2 = Style::default();
+        style_2.is_syntax_highlighted = !style_1.is_syntax_highlighted;
+
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Style(style_1)),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Style(style_2)),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        let expected = [("a", style_1), ("b", style_2), ("c", style_2)]
+            .iter()
+            .map(|(a, b)| (a.to_string(), *b))
+            .collect();
+
+        assert_eq!(resolve_style_references(edges), expected);
+    }
+
+    #[test]
+    fn test_resolve_style_references_2() {
+        let style_1 = Style::default();
+        let mut style_2 = Style::default();
+        style_2.is_syntax_highlighted = !style_1.is_syntax_highlighted;
+
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Reference("b".to_string())),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Style(style_1)),
+            ("d", StyleReference::Reference("b".to_string())),
+            ("e", StyleReference::Reference("a".to_string())),
+            ("f", StyleReference::Style(style_2)),
+            ("g", StyleReference::Reference("f".to_string())),
+            ("h", StyleReference::Reference("g".to_string())),
+            ("i", StyleReference::Reference("g".to_string())),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        let expected = [
+            ("a", style_1),
+            ("b", style_1),
+            ("c", style_1),
+            ("d", style_1),
+            ("e", style_1),
+            ("f", style_2),
+            ("g", style_2),
+            ("h", style_2),
+            ("i", style_2),
+        ]
+        .iter()
+        .map(|(a, b)| (a.to_string(), *b))
+        .collect();
+
+        assert_eq!(resolve_style_references(edges), expected);
+    }
+
+    #[test]
+    fn test_resolve_style_references_cycle() {
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Reference("b".to_string())),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Reference("a".to_string())),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        assert_eq!(
+            resolve_style_references(edges).keys().next().unwrap(),
+            "__cycle__"
+        );
+    }
+}
diff --git a/src/style.rs b/src/style.rs
index 8ce7887..b745cf3 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -5,6 +5,7 @@ use lazy_static::lazy_static;
 
 use crate::ansi;
 use crate::color;
+use crate::git_config::GitConfig;
 
 #[derive(Clone, Copy, PartialEq, Default)]
 pub struct Style {
@@ -128,11 +129,12 @@ impl Style {
 }
 
 /// Interpret `color_string` as a color specifier and return it painted accordingly.
-pub fn paint_color_string(
-    color_string: &str,
+pub fn paint_color_string<'a>(
+    color_string: &'a str,
     true_color: bool,
-) -> ansi_term::ANSIGenericString<str> {
-    if let Some(color) = color::parse_color(color_string, true_color) {
+    git_config: Option<&GitConfig>,
+) -> ansi_term::ANSIGenericString<'a, str> {
+    if let Some(color) = color::parse_color(color_string, true_color, git_config) {
         let style = ansi_term::Style {
             background: Some(color),
             ..ansi_term::Style::default()
diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs
index f115910..7669375 100644
--- a/src/subcommands/mod.rs
+++ b/src/subcommands/mod.rs
@@ -1,6 +1,7 @@
 pub mod diff;
 pub mod list_syntax_themes;
 mod sample_diff;
+pub mod show_colors;
 pub mod show_config;
 pub mod show_syntax_themes;
 pub mod show_themes;
diff --git a/src/subcommands/show_colors.rs b/src/subcommands/show_colors.rs
new file mode 100644
index 0000000..fd19437
--- /dev/null
+++ b/src/subcommands/show_colors.rs
@@ -0,0 +1,73 @@
+use crate::cli;
+use crate::color;
+use crate::colors;
+use crate::config;
+use crate::delta;
+use crate::git_config;
+use crate::paint;
+use crate::paint::BgShouldFill;
+use crate::style;
+use crate::utils::bat::assets::HighlightingAssets;
+use crate::utils::bat::output::{OutputType, PagingMode};
+
+#[cfg(not(tarpaulin_include))]
+pub fn show_colors() -> std::io::Result<()> {
+    use itertools::Itertools;
+
+    let assets = HighlightingAssets::new();
+    let opt = cli::Opt::from_args_and_git_config(git_config::GitConfig::try_create(), assets);
+    let config = config::Config::from(opt);
+
+    let mut output_type =
+        OutputType::from_mode(PagingMode::QuitIfOneScreen, None, &config).unwrap();
+    let writer = output_type.handle().unwrap();
+
+    let mut painter = paint::Painter::new(writer, &config);
+    painter.set_syntax(Some("ts"));
+    painter.set_highlighter();
+
+    let title_style = ansi_term::Style::new().bold();
+    let mut style = style::Style {
+        is_syntax_highlighted: true,
+        ..style::Style::default()
+    };
+    for (group, color_names) in colors::color_groups().iter().sorted() {
+        writeln!(painter.writer, "\n\n{}\n", title_style.paint(group))?;
+        for (color_name, hex) in color_names {
+            // Two syntax-highlighted lines without background color
+            style.ansi_term_style.background = None;
+            for line in [
+                r#"export function color(): string {{ return "none" }}"#,
+                r#"export function hex(): string {{ return "none" }}"#,
+            ] {
+                painter.syntax_highlight_and_paint_line(
+                    line,
+                    paint::StyleSectionSpecifier::Style(style),
+                    delta::State::HunkZero,
+                    BgShouldFill::default(),
+                )
+            }
+            // Two syntax-highlighted lines with background color
+            let color =
+                color::parse_color(color_name, config.true_color, config.git_config.as_ref())
+                    .unwrap();
+            style.ansi_term_style.background = Some(color);
+            for line in [
+                &format!(
+                    r#"export function color(): string {{ return "{}" }}"#,
+                    color_name
+                ),
+                &format!(r#"export function hex(): string {{ return "{}" }}"#, hex),
+            ] {
+                painter.syntax_highlight_and_paint_line(
+                    line,
+                    paint::StyleSectionSpecifier::Style(style),
+                    delta::State::HunkZero,
+                    BgShouldFill::default(),
+                )
+            }
+            painter.emit()?;
+        }
+    }
+    Ok(())
+}
diff --git a/src/subcommands/show_config.rs b/src/subcommands/show_config.rs
index ae45e87..cf86d09 100644
--- a/src/subcommands/show_config.rs
+++ b/src/subcommands/show_config.rs
@@ -31,7 +31,7 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
         blame_palette = config
             .blame_palette
             .iter()
-            .map(|s| style::paint_color_string(s, config.true_color))
+            .map(|s| style::paint_color_string(s, config.true_color, config.git_config.as_ref()))
             .join(" "),
         commit_style = config.commit_style.to_painted_string(),
         file_style = config.file_style.to_painted_string(),
@@ -117,7 +117,7 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
     max-line-length               = {max_line_length}
     line-fill-method              = {line_fill_method}
     navigate                      = {navigate}
-    navigate-regexp               = {navigate_regexp}
+    navigate-regex                = {navigate_regex}
     pager                         = {pager}
     paging                        = {paging_mode}
     side-by-side                  = {side_by_side}
@@ -132,9 +132,9 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
             BgFillMethod::Spaces => "spaces",
         },
         navigate = config.navigate,
-        navigate_regexp = match &config.navigate_regexp {
+        navigate_regex = match &config.navigate_regex {
             None => "".to_string(),
-            Some(s) => s.to_string(),
+            Some(s) => format_option_value(s.to_string()),
         },
         pager = config.pager.clone().unwrap_or_else(|| "none".to_string()),
         paging_mode = match config.paging_mode {
diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs
index a231168..62761c2 100644
--- a/src/utils/bat/output.rs
+++ b/src/utils/bat/output.rs
@@ -162,8 +162,7 @@ fn _make_process_from_less_path(
         p.env("LESSCHARSET", "UTF-8");
         p.env("LESSANSIENDCHARS", "mK");
         if config.navigate {
-            if let Ok(hist_file) = navigate::copy_less_hist_file_and_append_navigate_regexp(config)
-            {
+            if let Ok(hist_file) = navigate::copy_less_hist_file_and_append_navigate_regex(config) {
                 p.env("LESSHISTFILE", hist_file);
                 if config.show_themes {
                     p.arg("+n");
diff --git a/src/utils/process.rs b/src/utils/process.rs
index 163160f..ee145f8 100644
--- a/src/utils/process.rs
+++ b/src/utils/process.rs
@@ -1,6 +1,38 @@
+use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
+use std::path::Path;
 use sysinfo::{Pid, Process, ProcessExt, SystemExt};
 
+use lazy_static::lazy_static;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum CallingProcess {
+    GitShow(String),                             // (extension)
+    GitGrep((HashSet<String>, HashSet<String>)), // ((long_options, short_options))
+    OtherGrep,                                   // rg, grep, ag, ack, etc
+}
+
+pub fn calling_process() -> Option<Cow<'static, CallingProcess>> {
+    #[cfg(not(test))]
+    {
+        CACHED_CALLING_PROCESS
+            .as_ref()
+            .map(|proc| Cow::Borrowed(proc))
+    }
+    #[cfg(test)]
+    {
+        determine_calling_process().map(|proc| Cow::Owned(proc))
+    }
+}
+
+lazy_static! {
+    static ref CACHED_CALLING_PROCESS: Option<CallingProcess> = determine_calling_process();
+}
+
+fn determine_calling_process() -> Option<CallingProcess> {
+    calling_process_cmdline(ProcInfo::new(), describe_calling_process)
+}
+
 // Return value of `extract_args(args: &[String]) -> ProcessArgs<T>` function which is
 // passed to `calling_process_cmdline()`.
 #[derive(Debug, PartialEq)]
@@ -20,39 +52,6 @@ pub fn git_blame_filename_extension() -> Option<String> {
 mod blame {
     use super::*;
 
-    // Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
-    // `skip_this_plus_parameter` plus their respective next argument.
-    // Keep all arguments once a '--' is encountered.
-    // (Note that some arguments work with and without '=': '--foo' 'bar' / '--foo=bar')
-    fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
-        mut args_it: ArgsI,
-        skip_this_plus_parameter: SkipI,
-    ) -> Vec<&'a str>
-    where
-        ArgsI: Iterator<Item = &'a str>,
-        SkipI: Iterator<Item = &'b str>,
-    {
-        let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
-
-        let mut result = Vec::new();
-        loop {
-            match args_it.next() {
-                None => break result,
-                Some("--") => {
-                    result.extend(args_it);
-                    break result;
-                }
-                Some(arg) if arg_follows_space.contains(arg) => {
-                    let _skip_parameter = args_it.next();
-                }
-                Some(arg) if !arg.starts_with('-') => {
-                    result.push(arg);
-                }
-                Some(_) => { /* skip: --these -and --also=this */ }
-            }
-        }
-    }
-
     pub fn guess_git_blame_filename_extension(args: &[String]) -> ProcessArgs<String> {
         let all_args = args.iter().map(|s| s.as_str());
 
@@ -73,7 +72,121 @@ mod blame {
             _ => ProcessArgs::OtherProcess,
         }
     }
-} // mod blame
+}
+
+pub fn describe_calling_process(args: &[String]) -> ProcessArgs<CallingProcess> {
+    let mut args = args.iter().map(|s| s.as_str());
+
+    match args.next() {
+        Some(command) => match Path::new(command).file_stem() {
+            Some(s) if s.to_str() == Some("git") => {
+                let mut args = args.skip_while(|s| *s != "grep" && *s != "show");
+                match args.next() {
+                    Some("grep") => {
+                        ProcessArgs::Args(CallingProcess::GitGrep(parse_command_option_keys(args)))
+                    }
+                    Some("show") => {
+                        if let Some(extension) = get_git_show_file_extension(args) {
+                            ProcessArgs::Args(CallingProcess::GitShow(extension.to_string()))
+                        } else {
+                            // It's git show, but we failed to determine the
+                            // file extension. Don't look at any more processes.
+                            ProcessArgs::ArgError
+                        }
+                    }
+                    _ => {
+                        // It's git, but not a subcommand that we parse. Don't
+                        // look at any more processes.
+                        ProcessArgs::ArgError
+                    }
+                }
+            }
+            Some(s) => match s.to_str() {
+                Some("rg") | Some("grep") | Some("ack") | Some("ag") | Some("pt")
+                | Some("sift") | Some("ucg") => ProcessArgs::Args(CallingProcess::OtherGrep),
+                _ => {
+                    // It's not git, and it's not another grep tool. Keep
+                    // looking at other processes.
+                    ProcessArgs::OtherProcess
+                }
+            },
+            _ => {
+                // Could not parse file stem (not expected); keep looking at
+                // other processes.
+                ProcessArgs::OtherProcess
+            }
+        },
+        _ => {
+            // Empty arguments (not expected); keep looking.
+            ProcessArgs::OtherProcess
+        }
+    }
+}
+
+fn get_git_show_file_extension<'a>(args: impl Iterator<Item = &'a str>) -> Option<&'a str> {
+    if let Some(last_arg) = skip_uninteresting_args(args, "".split(' ')).last() {
+        // E.g. "HEAD~1:Makefile" or "775c3b8:./src/delta.rs"
+        match last_arg.split_once(':') {
+            Some((_, suffix)) => suffix.split('.').last(),
+            None => None,
+        }
+    } else {
+        None
+    }
+}
+
+// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
+// `skip_this_plus_parameter` plus their respective next argument.
+// Keep all arguments once a '--' is encountered.
+// (Note that some arguments work with and without '=': '--foo' 'bar' / '--foo=bar')
+fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
+    mut args_it: ArgsI,
+    skip_this_plus_parameter: SkipI,
+) -> Vec<&'a str>
+where
+    ArgsI: Iterator<Item = &'a str>,
+    SkipI: Iterator<Item = &'b str>,
+{
+    let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
+
+    let mut result = Vec::new();
+    loop {
+        match args_it.next() {
+            None => break result,
+            Some("--") => {
+                result.extend(args_it);
+                break result;
+            }
+            Some(arg) if arg_follows_space.contains(arg) => {
+                let _skip_parameter = args_it.next();
+            }
+            Some(arg) if !arg.starts_with('-') => {
+                result.push(arg);
+            }
+            Some(_) => { /* skip: --these -and --also=this */ }
+        }
+    }
+}
+
+// Given `--aa val -bc -d val e f -- ...` return
+// ({"--aa"}, {"-b", "-c", "-d"})
+fn parse_command_option_keys<'a>(
+    args: impl Iterator<Item = &'a str>,
+) -> (HashSet<String>, HashSet<String>) {
+    let mut longs = HashSet::new();
+    let mut shorts = HashSet::new();
+
+    for s in args {
+        if s == "--" {
+            break;
+        } else if s.starts_with("--") {
+            longs.insert(s.split('=').next().unwrap().to_owned());
+        } else if let Some(suffix) = s.strip_prefix('-') {
+            shorts.extend(suffix.chars().map(|c| format!("-{}", c)));
+        }
+    }
+    (longs, shorts)
+}
 
 struct ProcInfo {
     info: sysinfo::System,
@@ -190,7 +303,7 @@ trait ProcessInterface {
                 _ => None,
             })
             .min_by_key(|(distance, _)| *distance)
-            .map(|(_, ext)| ext);
+            .map(|(_, result)| result);
 
         cmdline_of_closest_matching_process
     }
@@ -221,13 +334,22 @@ where
     P: ProcessInterface,
     F: Fn(&[String]) -> ProcessArgs<T>,
 {
+    #[cfg(test)]
+    {
+        if let Some(args) = tests::cfg::WithArgs::get() {
+            match extract_args(&args) {
+                ProcessArgs::Args(result) => return Some(result),
+                _ => return None,
+            }
+        }
+    }
     let my_pid = info.my_pid();
 
     // 1) Try the parent process. If delta is set as the pager in git, then git is the parent process.
     let parent = info.parent_process(my_pid)?;
 
     match extract_args(parent.cmd()) {
-        ProcessArgs::Args(ext) => return Some(ext),
+        ProcessArgs::Args(result) => return Some(result),
         ProcessArgs::ArgError => return None,
 
         // 2) The parent process was something else, this can happen if git output is piped into delta, e.g.
@@ -236,8 +358,8 @@ where
         ProcessArgs::OtherProcess => {
             let sibling = info.naive_sibling_process(my_pid);
             if let Some(proc) = sibling {
-                if let ProcessArgs::Args(ext) = extract_args(proc.cmd()) {
-                    return Some(ext);
+                if let ProcessArgs::Args(result) = extract_args(proc.cmd()) {
+                    return Some(result);
                 }
             }
             // else try the fallback
@@ -295,12 +417,62 @@ where
 }
 
 #[cfg(test)]
-mod tests {
+pub mod tests {
     use super::blame::*;
     use super::*;
 
     use itertools::Itertools;
 
+    pub mod cfg {
+        use std::cell::RefCell;
+
+        #[derive(Debug, PartialEq)]
+        enum TlsState<T> {
+            Some(T),
+            None,
+            Invalid,
+        }
+
+        thread_local! {
+            static FAKE_ARGS: RefCell<TlsState<Vec<String>>> = RefCell::new(TlsState::None);
+        }
+
+        pub struct WithArgs {}
+        impl WithArgs {
+            pub fn new(args: &str) -> Self {
+                let string_vec = args.split(' ').map(str::to_owned).collect();
+                assert!(
+                    FAKE_ARGS.with(|a| a.replace(TlsState::Some(string_vec))) != TlsState::Invalid,
+                    "test logic error (in new): wrong WithArgs scope?"
+                );
+                WithArgs {}
+            }
+            pub fn get() -> Option<Vec<String>> {
+                FAKE_ARGS.with(|a| {
+                    let old_value = a.replace_with(|old_value| match old_value {
+                        TlsState::Some(_) => TlsState::Invalid,
+                        TlsState::None => TlsState::None,
+                        TlsState::Invalid => TlsState::Invalid,
+                    });
+
+                    match old_value {
+                        TlsState::Some(args) => Some(args),
+                        TlsState::None => None,
+                        TlsState::Invalid => {
+                            panic!("test logic error (in get): wrong WithArgs scope?")
+                        }
+                    }
+                })
+            }
+        }
+        impl Drop for WithArgs {
+            fn drop(&mut self) {
+                // clears an invalid state
+                FAKE_ARGS.with(|a| a.replace(TlsState::None));
+            }
+        }
+    }
+
     #[test]
     fn test_guess_git_blame_filename_extension() {
         use ProcessArgs::Args;
@@ -435,7 +607,39 @@ mod tests {
     }
 
     #[test]
-    fn test_blame_process_info_with_parent() {
+    fn test_process_testing() {
+        {
+            let _args = cfg::WithArgs::new(&"git blame hello");
+            assert_eq!(
+                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
+                Some("hello".into())
+            );
+        }
+        {
+            let _args = cfg::WithArgs::new(&"git blame world.txt");
+            assert_eq!(
+                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
+                Some("txt".into())
+            );
+        }
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_process_testing_assert() {
+        {
+            let _args = cfg::WithArgs::new(&"git blame do.not.panic");
+            assert_eq!(
+                calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension),
+                Some("panic".into())
+            );
+
+            calling_process_cmdline(ProcInfo::new(), blame::guess_git_blame_filename_extension);
+        }
+    }
+
+    #[test]
+    fn test_process_blame_info_with_parent() {
         let no_processes = MockProcInfo::with(&[]);
         assert_eq!(
             calling_process_cmdline(no_processes, blame::guess_git_blame_filename_extension),
@@ -465,8 +669,99 @@ mod tests {
     }
 
     #[test]
-    fn test_calling_process_cmdline() {
-        // Github runs CI tests for arm under qemu where where sysinfo can not find the parent processr.
+    fn test_describe_calling_process_grep() {
+        let no_processes = MockProcInfo::with(&[]);
+        assert_eq!(
+            calling_process_cmdline(no_processes, describe_calling_process),
+            None
+        );
+
+        let parent = MockProcInfo::with(&[
+            (2, 100, "-shell", None),
+            (3, 100, "git grep pattern hello.txt", Some(2)),
+            (4, 100, "delta", Some(3)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(parent, describe_calling_process),
+            Some(CallingProcess::GitGrep(([].into(), [].into())))
+        );
+
+        for other_grep_command in &[
+            "/usr/local/bin/rg pattern hello.txt",
+            "grep pattern hello.txt",
+            "/usr/bin/grep pattern hello.txt",
+            "ack.exe pattern hello.txt",
+        ] {
+            let parent = MockProcInfo::with(&[
+                (2, 100, "-shell", None),
+                (3, 100, other_grep_command, Some(2)),
+                (4, 100, "delta", Some(3)),
+            ]);
+            assert_eq!(
+                calling_process_cmdline(parent, describe_calling_process),
+                Some(CallingProcess::OtherGrep)
+            );
+        }
+
+        fn set(arg1: &[&str]) -> HashSet<String> {
+            arg1.iter().map(|&s| s.to_owned()).collect()
+        }
+
+        let git_grep_command =
+            "git grep -ab --function-context -n --show-function -W --foo=val pattern hello.txt";
+
+        let expected_result = Some(CallingProcess::GitGrep((
+            set(&["--function-context", "--show-function", "--foo"]),
+            set(&["-a", "-b", "-n", "-W"]),
+        )));
+
+        let parent = MockProcInfo::with(&[
+            (2, 100, "-shell", None),
+            (3, 100, git_grep_command, Some(2)),
+            (4, 100, "delta", Some(3)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(parent, describe_calling_process),
+            expected_result
+        );
+
+        let grandparent = MockProcInfo::with(&[
+            (2, 100, "-shell", None),
+            (3, 100, git_grep_command, Some(2)),
+            (4, 100, "call_delta.sh", Some(3)),
+            (5, 100, "delta", Some(4)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(grandparent, describe_calling_process),
+            expected_result
+        );
+    }
+
+    #[test]
+    fn test_describe_calling_process_git_show() {
+        for (command, expected_extension) in [
+            ("/usr/local/bin/git show 775c3b84:./src/hello.rs", "rs"),
+            ("/usr/local/bin/git show HEAD~1:Makefile", "Makefile"),
+            (
+                "git -c x.y=z show --abbrev-commit 775c3b84:./src/hello.bye.R",
+                "R",
+            ),
+        ] {
+            let parent = MockProcInfo::with(&[
+                (2, 100, "-shell", None),
+                (3, 100, command, Some(2)),
+                (4, 100, "delta", Some(3)),
+            ]);
+            assert_eq!(
+                calling_process_cmdline(parent, describe_calling_process),
+                Some(CallingProcess::GitShow(expected_extension.to_string())),
+            );
+        }
+    }
+
+    #[test]
+    fn test_process_calling_cmdline() {
+        // Github runs CI tests for arm under qemu where where sysinfo can not find the parent process.
         if std::env::vars().any(|(key, _)| key == "CROSS_RUNNER" || key == "QEMU_LD_PREFIX") {
             return;
         }
@@ -490,7 +785,7 @@ mod tests {
             }
         }
 
-        // Tests that caller is something like "cargo test" or "tarpaulin"
+        // Tests that caller is something like "cargo test" or "cargo tarpaulin"
         let find_test = |args: &[String]| find_calling_process(args, &["test", "tarpaulin"]);
         assert_eq!(calling_process_cmdline(info, find_test), Some(()));
 
diff --git a/src/utils/syntect.rs b/src/utils/syntect.rs
index 6f7e4af..99b358d 100644
--- a/src/utils/syntect.rs
+++ b/src/utils/syntect.rs
@@ -9,6 +9,15 @@ pub fn syntect_color_from_ansi_name(name: &str) -> Option<Color> {
     color::ansi_16_color_name_to_number(name).and_then(syntect_color_from_ansi_number)
 }
 
+pub fn syntect_color_from_name(name: &str) -> Option<Color> {
+    palette::named::from_str(name).map(|color| Color {
+        r: color.red,
+        g: color.green,
+        b: color.blue,
+        a: 0xFF,
+    })
+}
+
 /// Convert 8-bit ANSI code to #RGBA string with ANSI code in red channel and 0 in alpha channel.
 // See https://github.com/sharkdp/bat/pull/543
 pub fn syntect_color_from_ansi_number(n: u8) -> Option<Color> {
diff --git a/themes.gitconfig b/themes.gitconfig
index 707e71c..28764ee 100644
--- a/themes.gitconfig
+++ b/themes.gitconfig
@@ -75,6 +75,10 @@
 
 [delta "hoopoe"]
     # author: https://github.com/dandavison
+    dark-pink-style = normal "#ffc0c0"
+    pink-style = normal "#ffe0e0"
+    green-style = syntax "#d0ffd0"
+    dark-green-style = syntax "#a0efa0"
     commit-decoration-style = blue ol
     commit-style = raw
     file-style = omit
@@ -83,14 +87,14 @@
     hunk-header-line-number-style = "#067a00"
     hunk-header-style = file line-number syntax
     light = true
-    minus-emph-style = normal "#ffc0c0"
-    minus-empty-line-marker-style = normal "#ffe0e0"
-    minus-non-emph-style = normal "#ffe0e0"
-    minus-style = normal "#ffe0e0"
-    plus-emph-style = syntax "#a0efa0"
-    plus-empty-line-marker-style = normal "#d0ffd0"
-    plus-non-emph-style = syntax "#d0ffd0"
-    plus-style = syntax "#d0ffd0"
+    minus-emph-style = dark-pink-style
+    minus-empty-line-marker-style = minus-style
+    minus-non-emph-style = minus-style
+    minus-style = pink-style
+    plus-emph-style = dark-green-style
+    plus-empty-line-marker-style = plus-style
+    plus-non-emph-style = plus-style
+    plus-style = green-style
     syntax-theme = GitHub
     zero-style = syntax
 


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index e63f0ea..7d8c564 100644
--- a/src/tests/ansi_test_utils.rs
+++ b/src/tests/ansi_test_utils.rs
@@ -167,7 +167,13 @@ pub mod ansi_test_utils {
     ) -> bool {
         let line = output.lines().nth(line_number).unwrap();
         assert!(ansi::strip_ansi_codes(line).starts_with(expected_prefix));
-        let mut style = Style::from_str(expected_style, None, None, config.true_color, false);
+        let mut style = Style::from_str(
+            expected_style,
+            None,
+            None,
+            config.true_color,
+            config.git_config.as_ref(),
+        );
         if _4_bit_color {
             style.ansi_term_style.foreground = style
                 .ansi_term_style
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index a6d5f49..faecd2a 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -1471,7 +1471,7 @@ src/align.rs:71: impl<'a> Alignment<'a> { │
         let output = integration_test_utils::run_delta(example_diff, &config);
         let line = output.lines().nth(8).unwrap();
         if base_style_has_background_color {
-            let style = style::Style::from_str(base_style, None, None, true, false);
+            let style = style::Style::from_str(base_style, None, None, true, None);
             assert_eq!(
                 line,
                 &style