Repository: dandavison/delta
Pull request #512 (2021-02-01 20:31:59)

Option to set the background extension mode to ANSI or spaces
(Original topic: Fix right panel background extending beyond the truncation symbol)

In side-by-side mode, if `background_color_extends_to_terminal_width`
is set, the left panel color is extended via spaces, but the right
one via an ANSI sequence which instructs the terminal emulator to
fill the background color rightwards.

However due to the padding after the right panel the color will
extend beyond a possible truncation symbol:

`TEXT#AB→#`, not `TEXT#AB→ ` with `#` being the green background.

This is fixed by using the same logic as in the left panel for the
right panel.

---------------------

Before-After comparison in Konsole:

![sbs2](https://user-images.githubusercontent.com/78380144/106514451-8b6db800-64d4-11eb-9b8c-9e50d008414e.png)



th1000s: The background is now only filled with spaces if any line in a block gets truncated (or, soon, wrapped):

![ansi_spaces](https://user-images.githubusercontent.com/78380144/107890772-f11d6380-6f1a-11eb-8ee2-7c9b5b8e6df8.png)

This required moving some code written for wrapping here, I will now rebase the rest onto this.
dandavison: @th1000s can you explain why we ever need to fill with spaces? Sorry if I'm being slow! I don't think I've understood the motivation for this PR. I see this problem:

<table><tr><td><img width=500px src="https://user-images.githubusercontent.com/52205/107902673-1effa980-6f15-11eb-9a4d-6644827ab3ad.png" alt="image" /></td></tr></table>

But can't that be fixed by setting the background color of the continuation character to the fill color? I think it's correct that the background color extends beyond the continuation character; to my mind, the bug here is just that the background is white for one character, when it should be green.
th1000s: No problem. Hereafter "ANSI" means the ANSI Sequence which extends the color until the end of the line.

So, ANSI placed after the truncation symbol is the problem [1] (adapted from your screenshot), ANSI placed on lines which are not cut off is fine [2].

But, just selectively skipping ANSI at e.g. [3] but still emitting it where the line is not cut of [5] would result in a [jagged right edge](https://user-images.githubusercontent.com/78380144/106798758-b97f0380-665e-11eb-9888-2729328b73b4.png), here crossed out in red.

Not using ANSI or spaces doesn't work either, because then the cut off lines are fine [3], but shorter lines are not [4]. 

This means the only option is to not use ANSI for truncated lines, and filling up lines which are too short with **spaces**, i.e. [3] and [5] up to the dotted line.

![image](https://user-images.githubusercontent.com/78380144/107997043-2a6cd680-6fe2-11eb-87b0-a879cfd00b6d.png)


dandavison: Thanks very much for explaining that carefully. Would another alternative be to place the continuation character on the green (or whatever) background color instead of on the default (here white) background color? I was thinking that would then allow the code to be almost unchanged, with ANSI sequences used on all lines to fill the background color to the terminal width.
dandavison: Continuing my comment above, here are a couple of screenshots (on this branch at 306621bac7d3e5494dcd163fd8b9ca5f242132c5). Please correct me if I'm wrong but what I'm thinking is that even with this plan:

> not use ANSI for truncated lines, and filling up lines which are too short with spaces

we are still going to get "jagged right edges" in a sense, because the column to which the right fill extends will vary between  files and diff hunks. Hence my question about whether we should consider just placing everything, including the continuation character, on the colored background and always filling rightwards to the maximum extent, using the ANSI sequence in the right panel. 

<table><tr><td><img width=600px src="https://user-images.githubusercontent.com/52205/108228888-43be7180-710d-11eb-9e0b-5039b5c9dcc2.png" alt="image" /></td></tr></table>

<table><tr><td><img width=600px src="https://user-images.githubusercontent.com/52205/108228679-183b8700-710d-11eb-8bf4-9918289f29c0.png" alt="image" /></td></tr></table>
th1000s: Yes, making the background of a continuation character (`↵ / ↴ / →`) the same color as the rest of the line does make it visually more consistent. I will update it to that.

> jagged right edges

For larger visual component these are still there, that is true.

But back to the initial issue, for my personal taste the inconsistency of having the background _always_ end with exactly the last character on the red/left side, but having it extend further on the right side is worse -  hence my initial fix of _always_ padding with spaces.

And while this ruins terminal resizing/shrinking, in side-by-side mode that is going to happen much sooner anyhow. To help with that, maybe the pager could be started with `--chop-long-lines`. I also do not thing users change the size that often :). On the long run the only real fix is going to be using some terminal-ui which can redraw on resizes, maybe after some parts have been extracted into libdelta.
dandavison: > for my personal taste the inconsistency of having the background always end with exactly the last character on the red/left side, but having it extend further on the right side is worse - hence my initial fix of always padding with spaces.

I'm still not sure about this (in the sense that I'm genuinely not sure, not a euphemism for being against!). One consideration is that we haven't had any complaints from other users, and also this was the explicit motivation for the ANSI code in the first place. Is it possible for this decision to be made independently of adding the `side-by-side-wrapped` feature? Switching to spaces seems to be quite a large code change whereas I think changing the background color of the continuation character would be a small change. Is it possible for the `side-by-side-wrapped` branch to not be based on top of the more radical (switching to spaces) change?
th1000s: No problem, I'll adjust the background color of the continuation character to match that of the rest of the line. I'd also want to set the foreground color to blue to remove any possible ambiguities and still have it stand out a bit, similar how vim can highlight non-printable characters. Blue seems to be quite readable on both dark and light backgrounds (maybe tab-highlighting can work that way in delta as well).

![nonprint](https://user-images.githubusercontent.com/78380144/109295816-af03f400-782f-11eb-9732-f6462d8c3f56.png)

As for the for the ANSI code motivation, my impression was that these were more relevant for the unified diff view and were then re-used for the right panel of the side-by-side mode.
dandavison: > the inconsistency of having the background always end with exactly the last character on the red/left side, but having it extend further on the right side is worse -

OK, I think it's important that I understand this exactly. What I had thought was that side-by-side splits the user's terminal window exactly in half (modulo odd number width) and then applies equal (visual) widths of color in each panel. However, I'm looking at this, and I think I understand what you're saying: there seem to be two "extra" characters width of green beyond the truncation symbol on the right. So, why is that? Is the terminal width measurement or splitting calculation wrong?

<table><tr><td><img width=800px src="https://user-images.githubusercontent.com/52205/109306069-9f26e880-780c-11eb-9445-b98367529c9d.png" alt="image" /></td></tr></table>

> As for the for the ANSI code motivation, my impression was that these were more relevant for the unified diff view and were then re-used for the right panel of the side-by-side mode.

Yes, I think that's true. And I do agree with you that we want equal width color on each side (filling the full terminal width ideally, and modulo odd-number terminal widths)

dandavison: > No problem, I'll adjust the background color of the continuation character to match that of the rest of the line.

Great.

Regarding the spaces vs ANSI I am worried about causing you unnecessary work in the event that we both agree in the end that spaces is the right solution! So please, if you feel it makes sense to finish the conversation above before altering code, that would be absolutely fine of course.

> I'd also want to set the foreground color to blue to remove any possible ambiguities and still have it stand out a bit, similar how vim can highlight non-printable characters.

That sounds good.

> Blue seems to be quite readable on both dark and light backgrounds (maybe tab-highlighting can work that way in delta as well).

Right. Perhaps we'll want to make that color user-customizable? Side-by-side users will see it a lot (and delta seems to have taken it upon itself to be customizable to the last degree!)

th1000s: Indeed, the right padding is either 1 or 2, never 0 or 1, I thought it was a stylistic choice and never questioned it :)

If at most 1 line of padding is the goal, maybe that can be inserted in the middle, before the right line-numbers. That would make all the issues here disappear.

> unnecessary work

This patchset already makes using spaces or ANSI a function parameter (`BgFillWidth`: whether to fill the line at all, containing a `BgFillMethod`: ANSI or via spaces). And spaces are required either way for the left panel.

> user-customizable

Of course. The same goes for the continuation/termination chars, maybe someone likes other symbols more or wants to only use 7-bit ASCII.
dandavison: > If at most 1 line of padding is the goal, maybe that can be inserted in the middle, before the right line-numbers. That would make all the issues here disappear.

This sounds great. There is one thing that we need to bear in mind. Basically, I think that the fact that delta's width calculations are leaving extra columns on the right is due to this line:https://github.com/dandavison/delta/blob/e7d1e281ea539adb91a57c6945dea123fff1bd16/src/options/set.rs#L517-L518

I just wanted to highlight that line of code. Everything that follows in the rest of this comment is detail, just in case it is helpful.

-----------------------------------------------------------------

The issue being addressed there is that `less --status-column` inserts two columns on the left, and so if a user is using `less --status-column`, we have to be careful that delta does not overflow on the right.

To understand the history, the best issue to look at is this one: https://github.com/dandavison/delta/issues/115, which led to delta switching to use the ANSI fill-color-right sequence instead of manually padding with spaces. But that still leaves the issue for things like horizontal dividing lines, hence the `computed.available_terminal_width` subtraction line is still needed even after delta switched to using the ANSI fill-right sequence.

I think it may be relevant to mention that [bat](https://github.com/sharkdp/bat/issues/376) has a solution for this that we could consider introducing to delta. Bat's solution is to allow the user to declare how many columns are available to the application (bat/delta) via a +/- offset: 
```
--terminal-width <width>
            Explicitly set the width of the terminal instead of determining it automatically. If
            prefixed with '+' or '-', the value will be treated as an offset to the actual terminal
            width. See also: '--wrap'.
```

See https://github.com/sharkdp/bat/commit/dda27b253be828c9c52f040c815e3a5f45d80010 and https://github.com/sharkdp/bat/issues/376.

th1000s: I suspected there was a history to that behavior. Also, well, it is broken again, or maybe never worked with side-by-side mode:
Set the Terminal to any odd width of less than 173 (see `echo $COLUMNS`) and run `git log -p e1ef7f54 | LESS=--status-column target/release/delta -s` with exactly that commit.
th1000s: Here is my attempt to have my cake and eat it too: The data structure is now `PlusMinus` (MinusPlus reads weird), it is indexed by `Plus` or `Minus`, but in a side-by-side context (which are actually most of them) it can also be index with `PanelSide::{Left, Right}`.

I can now squash this as-is into the previous commits or drop the `Left`/`Right` indexing.
dandavison: > The data structure is now PlusMinus (MinusPlus reads weird), it is indexed by Plus or Minus, but in a side-by-side context (which are actually most of them) it can also be index with PanelSide::{Left, Right}.

Great, thanks!

> I can now squash this as-is into the previous commits or drop the Left/Right indexing.

I'd like to squash and keep the Left/Right indexing.

Looks like some tests are failing now, but once you've squashed and CI is green, I'll merge this.

```
failures:

---- features::side_by_side::tests::test_two_minus_lines_truncated stdout ----
thread 'features::side_by_side::tests::test_two_minus_lines_truncated' panicked at 'index out of bounds: the len is 0 but the index is 0', src/features/side_by_side.rs:335:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- features::side_by_side::tests::test_two_minus_lines stdout ----
thread 'features::side_by_side::tests::test_two_minus_lines' panicked at 'index out of bounds: the len is 0 but the index is 0', src/features/side_by_side.rs:335:18

---- features::side_by_side::tests::test_one_minus_one_plus_line stdout ----
thread 'features::side_by_side::tests::test_one_minus_one_plus_line' panicked at 'String mismatch encountered while superimposing style sections: ' ' vs 'b'', src/paint.rs:761:17


failures:
    features::side_by_side::tests::test_one_minus_one_plus_line
    features::side_by_side::tests::test_two_minus_lines
    features::side_by_side::tests::test_two_minus_lines_truncated
```
th1000s: Ah, I mean the other left of course, should be fixed!

Patch:
diff --git a/src/cli.rs b/src/cli.rs
index bb15b16..f0f6165 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -526,6 +526,11 @@ pub struct Opt {
     /// delta will be slow on very long lines (e.g. minified .js) if truncation is disabled.
     pub max_line_length: usize,
 
+    /// How to extend the background color to the end of the line in side-by-side mode. Can
+    /// be ansi (default) or spaces. Has no effect if --width=variable is given.
+    #[structopt(long = "line-fill-method")]
+    pub line_fill_method: Option<String>,
+
     /// The width of underline/overline decorations. Use --width=variable to extend decorations and
     /// background colors to the end of the text only. Otherwise background colors extend to the
     /// full terminal width.
diff --git a/src/config.rs b/src/config.rs
index 326153b..fbd5804 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -16,6 +16,7 @@ use crate::env;
 use crate::features::navigate;
 use crate::features::side_by_side;
 use crate::git_config::{GitConfig, GitConfigEntry};
+use crate::paint::BgFillMethod;
 use crate::style::{self, Style};
 
 pub struct Config {
@@ -51,6 +52,7 @@ pub struct Config {
     pub hyperlinks_file_link_format: String,
     pub inspect_raw_lines: cli::InspectRawLines,
     pub keep_plus_minus_markers: bool,
+    pub line_fill_method: BgFillMethod,
     pub line_numbers: bool,
     pub line_numbers_left_format: String,
     pub line_numbers_left_style: Style,
@@ -166,7 +168,7 @@ impl From<cli::Opt> for Config {
             process::exit(1);
         });
 
-        let side_by_side_data = side_by_side::SideBySideData::new(
+        let side_by_side_data = side_by_side::SideBySideData::new_sbs(
             &opt.computed.decorations_width,
             &opt.computed.available_terminal_width,
         );
@@ -187,6 +189,16 @@ impl From<cli::Opt> for Config {
         let file_renamed_label = opt.file_renamed_label;
         let hunk_label = opt.hunk_label;
 
+        let line_fill_method = match opt.line_fill_method.as_deref() {
+            // Note that "default" is not documented
+            Some("ansi") | Some("default") | None => BgFillMethod::TryAnsiSequence,
+            Some("spaces") => BgFillMethod::Spaces,
+            _ => {
+                eprintln!("Invalid option for line-fill-method: Expected \"ansi\" or \"spaces\".");
+                process::exit(1);
+            }
+        };
+
         let navigate_regexp = if opt.navigate || opt.show_themes {
             Some(navigate::make_navigate_regexp(
                 opt.show_themes,
@@ -245,6 +257,7 @@ impl From<cli::Opt> for Config {
             hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
             inspect_raw_lines: opt.computed.inspect_raw_lines,
             keep_plus_minus_markers: opt.keep_plus_minus_markers,
+            line_fill_method,
             line_numbers: opt.line_numbers,
             line_numbers_left_format: opt.line_numbers_left_format,
             line_numbers_left_style,
diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs
index c464f8c..0d28255 100644
--- a/src/features/line_numbers.rs
+++ b/src/features/line_numbers.rs
@@ -6,9 +6,10 @@ use regex::Regex;
 use crate::config;
 use crate::delta::State;
 use crate::features::hyperlinks;
-use crate::features::side_by_side;
+use crate::features::side_by_side::PanelSide;
 use crate::features::OptionValueFunction;
 use crate::format;
+use crate::plusminus::*;
 use crate::style::Style;
 
 pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
@@ -65,11 +66,11 @@ pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
 pub fn format_and_paint_line_numbers<'a>(
     line_numbers_data: &'a mut LineNumbersData,
     state: &State,
-    side_by_side_panel: Option<side_by_side::PanelSide>,
+    side_by_side_panel: Option<PanelSide>,
     config: &'a config::Config,
 ) -> Vec<ansi_term::ANSIGenericString<'a, str>> {
-    let m_ref = &mut line_numbers_data.hunk_minus_line_number;
-    let p_ref = &mut line_numbers_data.hunk_plus_line_number;
+    let nr_left = line_numbers_data.line_number[Minus];
+    let nr_right = line_numbers_data.line_number[Plus];
     let (minus_style, zero_style, plus_style) = (
         config.line_numbers_minus_style,
         config.line_numbers_zero_style,
@@ -77,20 +78,17 @@ pub fn format_and_paint_line_numbers<'a>(
     );
     let ((minus_number, plus_number), (minus_style, plus_style)) = match state {
         State::HunkMinus(_) => {
-            let m = *m_ref;
-            *m_ref += 1;
-            ((Some(m), None), (minus_style, plus_style))
+            line_numbers_data.line_number[Minus] += 1;
+            ((Some(nr_left), None), (minus_style, plus_style))
         }
         State::HunkZero => {
-            let (m, p) = (*m_ref, *p_ref);
-            *m_ref += 1;
-            *p_ref += 1;
-            ((Some(m), Some(p)), (zero_style, zero_style))
+            line_numbers_data.line_number[Minus] += 1;
+            line_numbers_data.line_number[Plus] += 1;
+            ((Some(nr_left), Some(nr_right)), (zero_style, zero_style))
         }
         State::HunkPlus(_) => {
-            let p = *p_ref;
-            *p_ref += 1;
-            ((None, Some(p)), (minus_style, plus_style))
+            line_numbers_data.line_number[Plus] += 1;
+            ((None, Some(nr_right)), (minus_style, plus_style))
         }
         _ => return Vec::new(),
     };
@@ -99,14 +97,14 @@ pub fn format_and_paint_line_numbers<'a>(
 
     let (emit_left, emit_right) = match (config.side_by_side, side_by_side_panel) {
         (false, _) => (true, true),
-        (true, Some(side_by_side::PanelSide::Left)) => (true, false),
-        (true, Some(side_by_side::PanelSide::Right)) => (false, true),
+        (true, Some(PanelSide::Left)) => (true, false),
+        (true, Some(PanelSide::Right)) => (false, true),
         (true, None) => unreachable!(),
     };
 
     if emit_left {
         formatted_numbers.extend(format_and_paint_line_number_field(
-            &line_numbers_data.left_format_data,
+            &line_numbers_data.format_data[PanelSide::Left],
             &config.line_numbers_left_style,
             minus_number,
             plus_number,
@@ -120,7 +118,7 @@ pub fn format_and_paint_line_numbers<'a>(
 
     if emit_right {
         formatted_numbers.extend(format_and_paint_line_number_field(
-            &line_numbers_data.right_format_data,
+            &line_numbers_data.format_data[PanelSide::Right],
             &config.line_numbers_right_style,
             minus_number,
             plus_number,
@@ -140,10 +138,8 @@ lazy_static! {
 
 #[derive(Default)]
 pub struct LineNumbersData<'a> {
-    pub left_format_data: format::FormatStringData<'a>,
-    pub right_format_data: format::FormatStringData<'a>,
-    pub hunk_minus_line_number: usize,
-    pub hunk_plus_line_number: usize,
+    pub format_data: PlusMinus<format::FormatStringData<'a>>,
+    pub line_number: PlusMinus<usize>,
     pub hunk_max_line_number_width: usize,
     pub plus_file: String,
 }
@@ -153,16 +149,11 @@ pub struct LineNumbersData<'a> {
 impl<'a> LineNumbersData<'a> {
     pub fn from_format_strings(left_format: &'a str, right_format: &'a str) -> LineNumbersData<'a> {
         Self {
-            left_format_data: format::parse_line_number_format(
-                left_format,
-                &*LINE_NUMBERS_PLACEHOLDER_REGEX,
+            format_data: PlusMinus::new(
+                format::parse_line_number_format(left_format, &*LINE_NUMBERS_PLACEHOLDER_REGEX),
+                format::parse_line_number_format(right_format, &*LINE_NUMBERS_PLACEHOLDER_REGEX),
             ),
-            right_format_data: format::parse_line_number_format(
-                right_format,
-                &*LINE_NUMBERS_PLACEHOLDER_REGEX,
-            ),
-            hunk_minus_line_number: 0,
-            hunk_plus_line_number: 0,
+            line_number: PlusMinus::new(0, 0),
             hunk_max_line_number_width: 0,
             plus_file: "".to_string(),
         }
@@ -172,8 +163,8 @@ impl<'a> LineNumbersData<'a> {
     pub fn initialize_hunk(&mut self, line_numbers: &[(usize, usize)], plus_file: String) {
         // Typically, line_numbers has length 2: an entry for the minus file, and one for the plus
         // file. In the case of merge commits, it may be longer.
-        self.hunk_minus_line_number = line_numbers[0].0;
-        self.hunk_plus_line_number = line_numbers[line_numbers.len() - 1].0;
+        self.line_number =
+            PlusMinus::new(line_numbers[0].0, line_numbers[line_numbers.len() - 1].0);
         let hunk_max_line_number = line_numbers.iter().map(|(n, d)| n + d).max().unwrap();
         self.hunk_max_line_number_width =
             1 + (hunk_max_line_number as f64).log10().floor() as usize;
@@ -392,7 +383,7 @@ pub mod tests {
         let mut lines = output.lines().skip(7);
         let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
         assert_eq!(strip_ansi_codes(line_1), " 1  ⋮    │a = 1");
-        assert_eq!(strip_ansi_codes(line_2), " 2  ⋮    │b = 2");
+        assert_eq!(strip_ansi_codes(line_2), " 2  ⋮    │b = 23456");
     }
 
     #[test]
@@ -416,7 +407,7 @@ pub mod tests {
         let mut lines = output.lines().skip(7);
         let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
         assert_eq!(strip_ansi_codes(line_1), "    ⋮ 1  │a = 1");
-        assert_eq!(strip_ansi_codes(line_2), "    ⋮ 2  │b = 2");
+        assert_eq!(strip_ansi_codes(line_2), "    ⋮ 2  │b = 234567");
     }
 
     #[test]
@@ -498,7 +489,7 @@ pub mod tests {
         let mut lines = output.lines().skip(5);
         let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
         assert_eq!(strip_ansi_codes(line_1), " 1  ⋮    │-a = 1");
-        assert_eq!(strip_ansi_codes(line_2), " 2  ⋮    │-b = 2");
+        assert_eq!(strip_ansi_codes(line_2), " 2  ⋮    │-b = 23456");
     }
 
     #[test]
@@ -523,7 +514,7 @@ index 223ca50..e69de29 100644
 +++ w/a.py
 @@ -1,2 +0,0 @@
 -a = 1
--b = 2
+-b = 23456
 ";
 
     pub const TWO_PLUS_LINES_DIFF: &str = "\
@@ -534,7 +525,7 @@ index 0000000..223ca50
 +++ i/a.py
 @@ -0,0 +1,2 @@
 +a = 1
-+b = 2
++b = 234567
 ";
 
     pub const ONE_MINUS_ONE_PLUS_LINE_DIFF: &str = "\
diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs
index 77f4893..6d85b7a 100644
--- a/src/features/side_by_side.rs
+++ b/src/features/side_by_side.rs
@@ -1,3 +1,5 @@
+use std::ops::{Index, IndexMut};
+
 use itertools::Itertools;
 use syntect::highlighting::Style as SyntectStyle;
 
@@ -8,6 +10,8 @@ use crate::delta::State;
 use crate::features::line_numbers;
 use crate::features::OptionValueFunction;
 use crate::paint::Painter;
+use crate::paint::{BgFillMethod, BgShouldFill};
+use crate::plusminus::*;
 use crate::style::Style;
 
 pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
@@ -24,62 +28,80 @@ pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
     ])
 }
 
+pub struct Panel {
+    pub width: usize,
+    pub offset: usize,
+}
+
+// Same as plusminus::PlusMinusIndex but with Left/Right instead
+// of Minus/Plus enum names. Only used in a side-by-side context.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum PanelSide {
     Left,
     Right,
 }
 
-pub struct SideBySideData {
-    pub left_panel: Panel,
-    pub right_panel: Panel,
+use PanelSide::*;
+
+impl<T> Index<PanelSide> for PlusMinus<T> {
+    type Output = T;
+    fn index(&self, side: PanelSide) -> &Self::Output {
+        match side {
+            PanelSide::Left => &self.minus,
+            PanelSide::Right => &self.plus,
+        }
+    }
 }
 
-pub struct Panel {
-    pub width: usize,
-    pub offset: usize,
+impl<T> IndexMut<PanelSide> for PlusMinus<T> {
+    fn index_mut(&mut self, side: PanelSide) -> &mut Self::Output {
+        match side {
+            PanelSide::Left => &mut self.minus,
+            PanelSide::Right => &mut self.plus,
+        }
+    }
 }
 
+pub type SideBySideData = PlusMinus<Panel>;
+
 impl SideBySideData {
-    pub fn new(decorations_width: &cli::Width, available_terminal_width: &usize) -> Self {
+    pub fn new_sbs(decorations_width: &cli::Width, available_terminal_width: &usize) -> Self {
         let panel_width = match decorations_width {
             cli::Width::Fixed(w) => w / 2,
             _ => available_terminal_width / 2,
         };
-        Self {
-            left_panel: Panel {
+        SideBySideData::new(
+            Panel {
                 width: panel_width,
                 offset: 0,
             },
-            right_panel: Panel {
+            Panel {
                 width: panel_width,
                 offset: 0,
             },
-        }
+        )
     }
 }
 
 /// Emit a sequence of minus and plus lines in side-by-side mode.
 #[allow(clippy::too_many_arguments)]
 pub fn paint_minus_and_plus_lines_side_by_side<'a>(
-    minus_syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
-    minus_diff_style_sections: Vec<Vec<(Style, &str)>>,
-    minus_states: Vec<&'a State>,
-    plus_syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
-    plus_diff_style_sections: Vec<Vec<(Style, &str)>>,
-    plus_states: Vec<&'a State>,
+    syntax_left_right: PlusMinus<Vec<Vec<(SyntectStyle, &str)>>>,
+    diff_left_right: PlusMinus<Vec<Vec<(Style, &str)>>>,
+    states_left_right: PlusMinus<Vec<&'a State>>,
     line_alignment: Vec<(Option<usize>, Option<usize>)>,
     output_buffer: &mut String,
     config: &Config,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
-    background_color_extends_to_terminal_width: Option<bool>,
+    background_color_extends_to_terminal_width: PlusMinus<BgShouldFill>,
 ) {
     for (minus_line_index, plus_line_index) in line_alignment {
         output_buffer.push_str(&paint_left_panel_minus_line(
             minus_line_index,
-            &minus_syntax_style_sections,
-            &minus_diff_style_sections,
+            &syntax_left_right[Left],
+            &diff_left_right[Left],
             match minus_line_index {
-                Some(i) => minus_states[i],
+                Some(i) => states_left_right[Left][i],
                 None => &State::HunkMinus(None),
             },
             line_numbers_data,
@@ -88,15 +110,15 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>(
             } else {
                 None
             },
-            background_color_extends_to_terminal_width,
+            background_color_extends_to_terminal_width[Left],
             config,
         ));
         output_buffer.push_str(&paint_right_panel_plus_line(
             plus_line_index,
-            &plus_syntax_style_sections,
-            &plus_diff_style_sections,
+            &syntax_left_right[Right],
+            &diff_left_right[Right],
             match plus_line_index {
-                Some(i) => plus_states[i],
+                Some(i) => states_left_right[Right][i],
                 None => &State::HunkPlus(None),
             },
             line_numbers_data,
@@ -105,7 +127,7 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>(
             } else {
                 None
             },
-            background_color_extends_to_terminal_width,
+            background_color_extends_to_terminal_width[Right],
             config,
         ));
         output_buffer.push('\n');
@@ -116,63 +138,50 @@ pub fn paint_minus_and_plus_lines_side_by_side<'a>(
 pub fn paint_zero_lines_side_by_side(
     syntax_style_sections: Vec<Vec<(SyntectStyle, &str)>>,
     diff_style_sections: Vec<Vec<(Style, &str)>>,
-    state: &State,
     output_buffer: &mut String,
     config: &Config,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
     painted_prefix: Option<ansi_term::ANSIString>,
-    background_color_extends_to_terminal_width: Option<bool>,
+    background_color_extends_to_terminal_width: BgShouldFill,
 ) {
+    let state = State::HunkZero;
+
     for (line_index, (syntax_sections, diff_sections)) in syntax_style_sections
         .iter()
         .zip_eq(diff_style_sections.iter())
         .enumerate()
     {
-        let (mut left_panel_line, left_panel_line_is_empty) = Painter::paint_line(
-            syntax_sections,
-            diff_sections,
-            state,
-            line_numbers_data,
-            Some(PanelSide::Left),
-            painted_prefix.clone(),
-            config,
-        );
-        // TODO: Avoid doing the superimpose_style_sections work twice.
-        // HACK: These are getting incremented twice, so knock them back down once.
-        if let Some(d) = line_numbers_data.as_mut() {
-            d.hunk_minus_line_number -= 1;
-            d.hunk_plus_line_number -= 1;
-        }
-        right_pad_left_panel_line(
-            &mut left_panel_line,
-            left_panel_line_is_empty,
-            Some(line_index),
-            &diff_style_sections,
-            &State::HunkZero,
-            background_color_extends_to_terminal_width,
-            config,
-        );
-        output_buffer.push_str(&left_panel_line);
+        for panel_side in &[Left, Right] {
+            let (mut panel_line, panel_line_is_empty) = Painter::paint_line(
+                syntax_sections,
+                diff_sections,
+                &state,
+                line_numbers_data,
+                Some(*panel_side),
+                painted_prefix.clone(),
+                config,
+            );
+            pad_panel_line_to_width(
+                &mut panel_line,
+                panel_line_is_empty,
+                Some(line_index),
+                &diff_style_sections,
+                &state,
+                *panel_side,
+                background_color_extends_to_terminal_width,
+                config,
+            );
+            output_buffer.push_str(&panel_line);
 
-        let (mut right_panel_line, right_panel_line_is_empty) = Painter::paint_line(
-            syntax_sections,
-            diff_sections,
-            state,
-            line_numbers_data,
-            Some(PanelSide::Right),
-            painted_prefix.clone(),
-            config,
-        );
-        right_fill_right_panel_line(
-            &mut right_panel_line,
-            right_panel_line_is_empty,
-            Some(line_index),
-            &diff_style_sections,
-            &State::HunkZero,
-            background_color_extends_to_terminal_width,
-            config,
-        );
-        output_buffer.push_str(&right_panel_line);
+            if panel_side == &Left {
+                // TODO: Avoid doing the superimpose_style_sections work twice.
+                // HACK: These are getting incremented twice, so knock them back down once.
+                if let Some(d) = line_numbers_data.as_mut() {
+                    d.line_number[Left] -= 1;
+                    d.line_number[Right] -= 1;
+                }
+            }
+        }
         output_buffer.push('\n');
     }
 }
@@ -185,7 +194,7 @@ fn paint_left_panel_minus_line<'a>(
     state: &'a State,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
     painted_prefix: Option<ansi_term::ANSIString>,
-    background_color_extends_to_terminal_width: Option<bool>,
+    background_color_extends_to_terminal_width: BgShouldFill,
     config: &Config,
 ) -> String {
     let (mut panel_line, panel_line_is_empty) = paint_minus_or_plus_panel_line(
@@ -194,16 +203,17 @@ fn paint_left_panel_minus_line<'a>(
         diff_style_sections,
         state,
         line_numbers_data,
-        PanelSide::Left,
+        Left,
         painted_prefix,
         config,
     );
-    right_pad_left_panel_line(
+    pad_panel_line_to_width(
         &mut panel_line,
         panel_line_is_empty,
         line_index,
         diff_style_sections,
         state,
+        Left,
         background_color_extends_to_terminal_width,
         config,
     );
@@ -219,7 +229,7 @@ fn paint_right_panel_plus_line<'a>(
     state: &'a State,
     line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
     painted_prefix: Option<ansi_term::ANSIString>,
-    background_color_extends_to_terminal_width: Option<bool>,
+    background_color_extends_to_terminal_width: BgShouldFill,
     config: &Config,
 ) -> String {
     let (mut panel_line, panel_line_is_empty) = paint_minus_or_plus_panel_line(
@@ -228,45 +238,57 @@ fn paint_right_panel_plus_line<'a>(
         diff_style_sections,
         state,
         line_numbers_data,
-        PanelSide::Right,
+        Right,
         painted_prefix,
         config,
     );
-    right_fill_right_panel_line(
+
+    pad_panel_line_to_width(
         &mut panel_line,
         panel_line_is_empty,
         line_index,
         diff_style_sections,
         state,
+        Right,
         background_color_extends_to_terminal_width,
         config,
     );
+
     panel_line
 }
 
-fn get_right_fill_style_for_left_panel(
+fn get_right_fill_style_for_panel(
     line_is_empty: bool,
     line_index: Option<usize>,
     diff_style_sections: &[Vec<(Style, &str)>],
     state: &State,
-    background_color_extends_to_terminal_width: Option<bool>,
+    panel_side: PanelSide,
+    background_color_extends_to_terminal_width: BgShouldFill,
     config: &Config,
-) -> Style {
+) -> (Option<BgFillMethod>, Style) {
+    // If in the the left panel then it must be filled with spaces.
+    let none_or_override = if panel_side == Left {
+        Some(BgFillMethod::Spaces)
+    } else {
+        None
+    };
+
     match (line_is_empty, line_index) {
-        (true, _) => config.null_style,
-        (false, None) => config.null_style,
+        (true, _) => (none_or_override, config.null_style),
+        (false, None) => (none_or_override, config.null_style),
         (false, Some(index)) => {
-            let (should_fill, fill_style) =
+            let (bg_fill_mode, fill_style) =
                 Painter::get_should_right_fill_background_color_and_fill_style(
                     &diff_style_sections[index],
                     state,
                     background_color_extends_to_terminal_width,
                     config,
                 );
-            if should_fill {
-                fill_style
-            } else {
-                config.null_style
+
+            match bg_fill_mode {
+                None => (none_or_override, config.null_style),
+                _ if panel_side == Left => (Some(BgFillMethod::Spaces), fill_style),
+                _ => (bg_fill_mode, fill_style),
             }
         }
     }
@@ -341,12 +363,12 @@ fn paint_minus_or_plus_panel_line(
         (s, t) if s == t => {}
         (State::HunkPlus(_), State::HunkMinus(_)) => {
             if let Some(d) = line_numbers_data.as_mut() {
-                d.hunk_minus_line_number -= 1;
+                d.line_number[Left] -= 1;
             }
         }
         (State::HunkMinus(_), State::HunkPlus(_)) => {
             if let Some(d) = line_numbers_data.as_mut() {
-                d.hunk_plus_line_number -= 1;
+                d.line_number[Right] -= 1;
             }
         }
         _ => unreachable!(),
@@ -354,21 +376,20 @@ fn paint_minus_or_plus_panel_line(
     (line, line_is_empty)
 }
 
-/// Right-pad a line in the left panel with (possibly painted) spaces. A line in the left panel is
-/// either a minus line or a zero line.
-#[allow(clippy::comparison_chain)]
-fn right_pad_left_panel_line(
+/// Right-fill the background color of a line in a panel. If in the left panel this is always
+/// done with spaces. The right panel can be filled with spaces or using ANSI sequences
+/// instructing the terminal emulator to fill the background color rightwards.
+#[allow(clippy::too_many_arguments, clippy::comparison_chain)]
+fn pad_panel_line_to_width(
     panel_line: &mut String,
     panel_line_is_empty: bool,
     line_index: Option<usize>,
     diff_style_sections: &[Vec<(Style, &str)>],
     state: &State,
-    background_color_extends_to_terminal_width: Option<bool>,
+    panel_side: PanelSide,
+    background_color_extends_to_terminal_width: BgShouldFill,
     config: &Config,
 ) {
-    // The left panel uses spaces to pad to the midpoint. This differs from the right panel,
-    // and from the non-side-by-side implementation.
-
     // Emit empty line marker if the panel line is empty but not empty-by-construction. IOW if the
     // other panel contains a real line, and we are currently emitting an empty counterpart panel
     // to form the other half of the line, then don't emit the empty line marker.
@@ -379,81 +400,46 @@ fn right_pad_left_panel_line(
                 panel_line,
                 Some(" "),
             ),
+            State::HunkPlus(_) => Painter::mark_empty_line(
+                &config.plus_empty_line_marker_style,
+                panel_line,
+                Some(" "),
+            ),
             State::HunkZero => {}
             _ => unreachable!(),
         };
     };
-    // Pad with (maybe painted) spaces to the panel width.
+
     let text_width = ansi::measure_text_width(panel_line);
-    let panel_width = config.side_by_side_data.left_panel.width;
-    if text_width < panel_width {
-        let fill_style = get_right_fill_style_for_left_panel(
-            panel_line_is_empty,
-            line_index,
-            diff_style_sections,
-            state,
-            background_color_extends_to_terminal_width,
-            config,
-        );
-        panel_line.push_str(
-            &fill_style
-                .paint(" ".repeat(panel_width - text_width))
-                .to_string(),
-        );
-    } else if text_width > panel_width {
+    let panel_width = config.side_by_side_data[panel_side].width;
+
+    if text_width > panel_width {
         *panel_line =
             ansi::truncate_str(panel_line, panel_width, &config.truncation_symbol).to_string();
     }
-}
 
-/// Right-fill the background color of a line in the right panel. A line in the right panel is
-/// either a zero line or a plus line. The fill is achieved using ANSI sequences instructing the
-/// terminal emulator to fill the background color rightwards; it does not involve appending spaces
-/// to the line.
-fn right_fill_right_panel_line(
-    panel_line: &mut String,
-    panel_line_is_empty: bool,
-    line_index: Option<usize>,
-    diff_style_sections: &[Vec<(Style, &str)>],
-    state: &State,
-    background_color_extends_to_terminal_width: Option<bool>,
-    config: &Config,
-) {
-    *panel_line = ansi::truncate_str(
-        panel_line,
-        config.side_by_side_data.right_panel.width,
-        &config.truncation_symbol,
-    )
-    .to_string();
-
-    // Unlike `right_pad_left_panel_line`, the line-end emissions here are basically the same as
-    // the non side-by-side implementation in Painter::paint_lines.
-    let (should_right_fill_background_color, fill_style) = if let Some(index) = line_index {
-        Painter::get_should_right_fill_background_color_and_fill_style(
-            &diff_style_sections[index],
-            state,
-            background_color_extends_to_terminal_width,
-            config,
-        )
-    } else {
-        (false, config.null_style)
-    };
+    let (bg_fill_mode, fill_style) = get_right_fill_style_for_panel(
+        panel_line_is_empty,
+        line_index,
+        diff_style_sections,
+        state,
+        panel_side,
+        background_color_extends_to_terminal_width,
+        config,
+    );
 
-    if should_right_fill_background_color {
-        Painter::right_fill_background_color(panel_line, fill_style);
-    } else if panel_line_is_empty && line_index.is_some() {
-        // Emit empty line marker when the panel line is empty but not empty-by-construction. See
-        // parallel comment in `paint_left_panel_minus_line`.
-        match state {
-            State::HunkPlus(_) => Painter::mark_empty_line(
-                &config.plus_empty_line_marker_style,
-                panel_line,
-                Some(" "),
-            ),
-            State::HunkZero => {}
-            _ => unreachable!(),
+    match bg_fill_mode {
+        Some(BgFillMethod::TryAnsiSequence) => {
+            Painter::right_fill_background_color(panel_line, fill_style)
         }
-    };
+        Some(BgFillMethod::Spaces) if text_width >= panel_width => (),
+        Some(BgFillMethod::Spaces) => panel_line.push_str(
+            &fill_style
+                .paint(" ".repeat(panel_width - text_width))
+                .to_string(),
+        ),
+        None => (),
+    }
 }
 
 #[cfg(test)]
@@ -469,7 +455,23 @@ pub mod tests {
         let mut lines = output.lines().skip(7);
         let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
         assert_eq!("│ 1  │a = 1         │    │", strip_ansi_codes(line_1));
-        assert_eq!("│ 2  │b = 2         │    │", strip_ansi_codes(line_2));
+        assert_eq!("│ 2  │b = 23456     │    │", strip_ansi_codes(line_2));
+    }
+
+    #[test]
+    fn test_two_minus_lines_truncated() {
+        let mut config = make_config_from_args(&[
+            "--side-by-side",
+            "--width",
+            "28",
+            "--line-fill-method=spaces",
+        ]);
+        config.truncation_symbol = ">".into();
+        let output = run_delta(TWO_MINUS_LINES_DIFF, &config);
+        let mut lines = output.lines().skip(7);
+        let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
+        assert_eq!("│ 1  │a = 1   │    │", strip_ansi_codes(line_1));
+        assert_eq!("│ 2  │b = 234>│    │", strip_ansi_codes(line_2));
     }
 
     #[test]
@@ -478,8 +480,36 @@ pub mod tests {
         let output = run_delta(TWO_PLUS_LINES_DIFF, &config);
         let mut lines = output.lines().skip(7);
         let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
-        assert_eq!("│    │              │ 1  │a = 1", strip_ansi_codes(line_1));
-        assert_eq!("│    │              │ 2  │b = 2", strip_ansi_codes(line_2));
+        let sac = strip_ansi_codes; // alias to help with `cargo fmt`-ing:
+        assert_eq!("│    │              │ 1  │a = 1", sac(line_1));
+        assert_eq!("│    │              │ 2  │b = 234567", sac(line_2));
+    }
+
+    #[test]
+    fn test_two_plus_lines_truncated() {
+        let mut config = make_config_from_args(&[
+            "--side-by-side",
+            "--width",
+            "30",
+            "--line-fill-method=spaces",
+        ]);
+        config.truncation_symbol = ">".into();
+        let output = run_delta(TWO_PLUS_LINES_DIFF, &config);
+        let mut lines = output.lines().skip(7);
+        let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
+        assert_eq!("│    │         │ 1  │a = 1    ", strip_ansi_codes(line_1));
+        assert_eq!("│    │         │ 2  │b = 2345>", strip_ansi_codes(line_2));
+    }
+
+    #[test]
+    fn test_two_plus_lines_exact_fit() {
+        let mut config = make_config_from_args(&["--side-by-side", "--width", "32"]);
+        config.truncation_symbol = ">".into();
+        let output = run_delta(TWO_PLUS_LINES_DIFF, &config);
+        let mut lines = output.lines().skip(7);
+        let (line_1, line_2) = (lines.next().unwrap(), lines.next().unwrap());
+        assert_eq!("│    │          │ 1  │a = 1", strip_ansi_codes(line_1));
+        assert_eq!("│    │          │ 2  │b = 234567", strip_ansi_codes(line_2));
     }
 
     #[test]
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs
index d748630..af1057d 100644
--- a/src/handlers/blame.rs
+++ b/src/handlers/blame.rs
@@ -6,6 +6,7 @@ use crate::color;
 use crate::config;
 use crate::delta::{self, State, StateMachine};
 use crate::format;
+use crate::paint::BgShouldFill;
 use crate::style::Style;
 
 impl<'a> StateMachine<'a> {
@@ -56,7 +57,7 @@ impl<'a> StateMachine<'a> {
                     blame.code,
                     style,
                     self.state.clone(),
-                    true,
+                    BgShouldFill::default(),
                 );
                 handled_line = true
             }
diff --git a/src/handlers/hunk_header.rs b/src/handlers/hunk_header.rs
index 1d10d4f..b601ef5 100644
--- a/src/handlers/hunk_header.rs
+++ b/src/handlers/hunk_header.rs
@@ -28,7 +28,7 @@ use super::draw;
 use crate::config::Config;
 use crate::delta::{self, State, StateMachine};
 use crate::features;
-use crate::paint::Painter;
+use crate::paint::{BgShouldFill, Painter};
 use crate::style::DecorationStyle;
 
 impl<'a> StateMachine<'a> {
@@ -247,7 +247,7 @@ fn write_to_output_buffer(
             &line,
             config.hunk_header_style,
             delta::State::HunkHeader("".to_owned(), "".to_owned()),
-            false,
+            BgShouldFill::No,
         );
         painter.output_buffer.pop(); // trim newline
     }
diff --git a/src/main.rs b/src/main.rs
index 08d31fd..1fdf936 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,6 +20,7 @@ mod handlers;
 mod options;
 mod paint;
 mod parse_style;
+mod plusminus;
 mod style;
 mod subcommands;
 mod syntect_color;
diff --git a/src/options/set.rs b/src/options/set.rs
index a896820..1f53326 100644
--- a/src/options/set.rs
+++ b/src/options/set.rs
@@ -158,6 +158,7 @@ pub fn set_options(
             minus_non_emph_style,
             minus_non_emph_style,
             navigate,
+            line_fill_method,
             line_numbers,
             line_numbers_left_format,
             line_numbers_left_style,
diff --git a/src/paint.rs b/src/paint.rs
index a575d3b..3015187 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -12,7 +12,9 @@ use crate::delta::State;
 use crate::edits;
 use crate::features::line_numbers;
 use crate::features::side_by_side;
+use crate::features::side_by_side::PanelSide;
 use crate::paint::superimpose_style_sections::superimpose_style_sections;
+use crate::plusminus::*;
 use crate::style::Style;
 
 pub struct Painter<'a> {
@@ -26,6 +28,28 @@ pub struct Painter<'a> {
     pub line_numbers_data: line_numbers::LineNumbersData<'a>,
 }
 
+// How the background of a line is filled up to the end
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum BgFillMethod {
+    // Fill the background with ANSI spaces if possible,
+    // but might fallback to Spaces (e.g. in the left side-by-side panel)
+    TryAnsiSequence,
+    Spaces,
+}
+
+// If the background of a line extends to the end, and if configured to do so, how.
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum BgShouldFill {
+    With(BgFillMethod),
+    No,
+}
+
+impl Default for BgShouldFill {
+    fn default() -> Self {
+        BgShouldFill::With(BgFillMethod::TryAnsiSequence)
+    }
+}
+
 impl<'a> Painter<'a> {
     pub fn new(writer: &'a mut dyn Write, config: &'a config::Config) -> Self {
         let default_syntax = Self::get_syntax(&config.syntax_set, None);
@@ -138,18 +162,36 @@ impl<'a> Painter<'a> {
             Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config);
 
         if self.config.side_by_side {
-            side_by_side::paint_minus_and_plus_lines_side_by_side(
+            let syntax_left_right = PlusMinus::new(
                 minus_line_syntax_style_sections,
-                minus_line_diff_style_sections,
-                self.minus_lines.iter().map(|(_, state)| state).collect(),
                 plus_line_syntax_style_sections,
+            );
+            let diff_left_right = PlusMinus::new(
+                minus_line_diff_style_sections,
                 plus_line_diff_style_sections,
+            );
+
+            let states_left_right = PlusMinus::new(
+                self.minus_lines.iter().map(|(_, state)| state).collect(),
                 self.plus_lines.iter().map(|(_, state)| state).collect(),
+            );
+
+            let bg_fill_left_right = PlusMinus::new(
+                // Using an ANSI sequence to fill the left panel would not work
+                BgShouldFill::With(BgFillMethod::Spaces),
+                // Use the configured method for the right panel
+                BgShouldFill::With(self.config.line_fill_method),
+            );
+
+            side_by_side::paint_minus_and_plus_lines_side_by_side(
+                syntax_left_right,
+                diff_left_right,
+                states_left_right,
                 line_alignment,
                 &mut self.output_buffer,
                 self.config,
                 &mut Some(&mut self.line_numbers_data),
-                None,
+                bg_fill_left_right,
             );
         } else {
             if !self.minus_lines.is_empty() {
@@ -166,7 +208,7 @@ impl<'a> Painter<'a> {
                         None
                     },
                     Some(self.config.minus_empty_line_marker_style),
-                    None,
+                    BgShouldFill::default(),
                 );
             }
             if !self.plus_lines.is_empty() {
@@ -183,7 +225,7 @@ impl<'a> Painter<'a> {
                         None
                     },
                     Some(self.config.plus_empty_line_marker_style),
-                    None,
+                    BgShouldFill::default(),
                 );
             }
         }
@@ -211,12 +253,11 @@ impl<'a> Painter<'a> {
             side_by_side::paint_zero_lines_side_by_side(
                 syntax_style_sections,
                 vec![diff_style_sections],
-                &State::HunkZero,
                 &mut self.output_buffer,
                 self.config,
                 &mut Some(&mut self.line_numbers_data),
                 painted_prefix,
-                None,
+                BgShouldFill::With(BgFillMethod::Spaces),
             );
         } else {
             Painter::paint_lines(
@@ -228,7 +269,7 @@ impl<'a> Painter<'a> {
                 &mut Some(&mut self.line_numbers_data),
                 painted_prefix,
                 None,
-                None,
+                BgShouldFill::With(BgFillMethod::Spaces),
             );
         }
     }
@@ -245,7 +286,7 @@ impl<'a> Painter<'a> {
         line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
         painted_prefix: Option<ansi_term::ANSIString>,
         empty_line_style: Option<Style>, // a style with background color to highlight an empty line
-        background_color_extends_to_terminal_width: Option<bool>,
+        background_color_extends_to_terminal_width: BgShouldFill,
     ) {
         // There's some unfortunate hackery going on here for two reasons:
         //
@@ -268,15 +309,23 @@ impl<'a> Painter<'a> {
                 painted_prefix.clone(),
                 config,
             );
-            let (should_right_fill_background_color, fill_style) =
+            let (bg_fill_mode, fill_style) =
                 Painter::get_should_right_fill_background_color_and_fill_style(
                     diff_sections,
                     state,
                     background_color_extends_to_terminal_width,
                     config,
                 );
-            if should_right_fill_background_color {
+
+            if let Some(BgFillMethod::TryAnsiSequence) = bg_fill_mode {
                 Painter::right_fill_background_color(&mut line, fill_style);
+            } else if let Some(BgFillMethod::Spaces) = bg_fill_mode {
+                let text_width = ansi::measure_text_width(&line);
+                line.push_str(
+                    &fill_style
+                        .paint(" ".repeat(config.available_terminal_width - text_width))
+                        .to_string(),
+                );
             } else if line_is_empty {
                 if let Some(empty_line_style) = empty_line_style {
                     Painter::mark_empty_line(
@@ -286,6 +335,7 @@ impl<'a> Painter<'a> {
                     );
                 }
             };
+
             output_buffer.push_str(&line);
             output_buffer.push('\n');
         }
@@ -297,7 +347,7 @@ impl<'a> Painter<'a> {
         line: &str,
         style: Style,
         state: State,
-        background_color_extends_to_terminal_width: bool,
+        background_color_extends_to_terminal_width: BgShouldFill,
     ) {
         let lines = vec![(self.expand_tabs(line.graphemes(true)), state.clone())];
         let syntax_style_sections = Painter::get_syntax_style_sections_for_lines(
@@ -316,7 +366,7 @@ impl<'a> Painter<'a> {
             &mut None,
             None,
             None,
-            Some(background_color_extends_to_terminal_width),
+            background_color_extends_to_terminal_width,
         );
     }
 
@@ -325,9 +375,9 @@ impl<'a> Painter<'a> {
     pub fn get_should_right_fill_background_color_and_fill_style(
         diff_sections: &[(Style, &str)],
         state: &State,
-        background_color_extends_to_terminal_width: Option<bool>,
+        background_color_extends_to_terminal_width: BgShouldFill,
         config: &config::Config,
-    ) -> (bool, Style) {
+    ) -> (Option<BgFillMethod>, Style) {
         // style:          for right fill if line contains no emph sections
         // non_emph_style: for right fill if line contains emph sections
         let (style, non_emph_style) = match state {
@@ -366,10 +416,20 @@ impl<'a> Painter<'a> {
         } else {
             style
         };
-        let should_right_fill_background_color = fill_style.get_background_color().is_some()
-            && background_color_extends_to_terminal_width
-                .unwrap_or(config.background_color_extends_to_terminal_width);
-        (should_right_fill_background_color, fill_style)
+
+        match (
+            fill_style.get_background_color().is_some(),
+            background_color_extends_to_terminal_width,
+        ) {
+            (false, _) | (_, BgShouldFill::No) => (None, fill_style),
+            (_, BgShouldFill::With(bgmode)) => {
+                if config.background_color_extends_to_terminal_width {
+                    (Some(bgmode), fill_style)
+                } else {
+                    (None, fill_style)
+                }
+            }
+        }
     }
 
     /// Emit line with ANSI sequences that extend the background color to the terminal width.
@@ -405,7 +465,7 @@ impl<'a> Painter<'a> {
         diff_sections: &[(Style, &str)],
         state: &State,
         line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
-        side_by_side_panel: Option<side_by_side::PanelSide>,
+        side_by_side_panel: Option<PanelSide>,
         painted_prefix: Option<ansi_term::ANSIString>,
         config: &config::Config,
     ) -> (String, bool) {
diff --git a/src/plusminus.rs b/src/plusminus.rs
new file mode 100644
index 0000000..0cdeaea
--- /dev/null
+++ b/src/plusminus.rs
@@ -0,0 +1,51 @@
+use std::ops::{Index, IndexMut};
+
+// Struct to represent data related to removed/minus and added/plus lines
+// which can be indexed with PlusMinusIndex::{Minus, Plus}.
+#[derive(Debug, PartialEq, Eq)]
+pub struct PlusMinus<T> {
+    pub minus: T,
+    pub plus: T,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PlusMinusIndex {
+    Minus,
+    Plus,
+}
+
+pub use PlusMinusIndex::*;
+
+impl<T> Index<PlusMinusIndex> for PlusMinus<T> {
+    type Output = T;
+    fn index(&self, side: PlusMinusIndex) -> &Self::Output {
+        match side {
+            Minus => &self.minus,
+            Plus => &self.plus,
+        }
+    }
+}
+
+impl<T> IndexMut<PlusMinusIndex> for PlusMinus<T> {
+    fn index_mut(&mut self, side: PlusMinusIndex) -> &mut Self::Output {
+        match side {
+            Minus => &mut self.minus,
+            Plus => &mut self.plus,
+        }
+    }
+}
+
+impl<T> PlusMinus<T> {
+    pub fn new(minus: T, plus: T) -> Self {
+        PlusMinus { minus, plus }
+    }
+}
+
+impl<T: Default> Default for PlusMinus<T> {
+    fn default() -> Self {
+        Self {
+            minus: T::default(),
+            plus: T::default(),
+        }
+    }
+}
diff --git a/src/subcommands/show_config.rs b/src/subcommands/show_config.rs
index 2202256..254c5e3 100644
--- a/src/subcommands/show_config.rs
+++ b/src/subcommands/show_config.rs
@@ -3,6 +3,7 @@ use std::io::Write;
 use crate::bat_utils::output::PagingMode;
 use crate::cli;
 use crate::config;
+use crate::paint::BgFillMethod;
 
 pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::Result<()> {
     // styles first
@@ -99,6 +100,7 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
         writer,
         "    max-line-distance             = {max_line_distance}
     max-line-length               = {max_line_length}
+    line-fill-method        = {line_fill_method}
     navigate                      = {navigate}
     navigate-regexp               = {navigate_regexp}
     pager                         = {pager}
@@ -110,6 +112,10 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
     word-diff-regex               = {tokenization_regex}",
         max_line_distance = config.max_line_distance,
         max_line_length = config.max_line_length,
+        line_fill_method = match config.line_fill_method {
+            BgFillMethod::TryAnsiSequence => "ansi",
+            BgFillMethod::Spaces => "spaces",
+        },
         navigate = config.navigate,
         navigate_regexp = match &config.navigate_regexp {
             None => "".to_string(),


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index d2db3ee..3f4fbe9 100644
--- a/src/tests/ansi_test_utils.rs
+++ b/src/tests/ansi_test_utils.rs
@@ -152,7 +152,7 @@ pub mod ansi_test_utils {
             &mut None,
             Some(config.null_style.paint(prefix)),
             None,
-            None,
+            paint::BgShouldFill::default(),
         );
         output_buffer
     }