Full Code of so-fancy/diff-so-fancy for AI

next 503456ec17d1 cached
61 files
145.6 KB
54.5k tokens
1 requests
Download .txt
Repository: so-fancy/diff-so-fancy
Branch: next
Commit: 503456ec17d1
Files: 61
Total size: 145.6 KB

Directory structure:
gitextract_6y16t5y0/

├── .circleci/
│   └── config.yml
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── awesomebot.yml
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── diff-so-fancy
├── diff-so-fancy.plugin.zsh
├── docs/
│   └── diff-so-fancy.1
├── hacking-and-testing.md
├── history.md
├── lib/
│   └── DiffHighlight.pm
├── package.json
├── pro-tips.md
├── report-bug.sh
├── reporting-bugs.md
├── test/
│   ├── bugs.bats
│   ├── diff-so-fancy.bats
│   ├── fixtures/
│   │   ├── add_empty_file.diff
│   │   ├── add_remove_empty_lines.diff
│   │   ├── ansi_reset_no_number.diff
│   │   ├── binary-modified.diff
│   │   ├── chromium-modaltoelement.diff
│   │   ├── complex-hunks.diff
│   │   ├── diff_recursive.diff
│   │   ├── dotfiles.diff
│   │   ├── file-moves.diff
│   │   ├── file-perms.diff
│   │   ├── file-rename.diff
│   │   ├── file_copy.diff
│   │   ├── file_with_space.diff
│   │   ├── first-three-line.diff
│   │   ├── hg.diff
│   │   ├── hunk_no_comma.diff
│   │   ├── latin1.diff
│   │   ├── leading-dashes.diff
│   │   ├── ls-function.diff
│   │   ├── mnemonicprefix.diff
│   │   ├── move_with_content_change.diff
│   │   ├── noprefix.diff
│   │   ├── recursive_default_as_mercurial.diff
│   │   ├── recursive_longhand_as_mercurial.diff
│   │   ├── remove_empty_file.diff
│   │   ├── remove_slashn_eof.diff
│   │   ├── single-line-remove.diff
│   │   ├── truecolor.diff
│   │   ├── unicode.diff
│   │   └── weird.diff
│   ├── git-config.bats
│   ├── git_ansi_color.pl
│   └── test_helper/
│       └── util.bash
├── third_party/
│   ├── ansi-reveal/
│   │   └── ansi-reveal.pl
│   ├── build_fatpack/
│   │   └── build.pl
│   ├── cli_bench/
│   │   └── cli_bench.pl
│   └── term-colors/
│       └── term-colors.pl
└── update-deps.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
  build:
    machine: true
    steps:
      - checkout
      - run:
          name:
          command: |
            sudo apt-get -y update
            sudo apt-get -y install shellcheck git

      - run: git submodule sync
      - run: git submodule update --init
      - run:
          name: tests
          command: |
            export TERM=dumb && ./test/bats/bin/bats test
            shellcheck *.sh


================================================
FILE: .github/dependabot.yml
================================================
---
# Use `allow` to specify which dependencies to maintain

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/awesomebot.yml
================================================
---
name: Check links in README.md

on:
  # Run daily
  schedule: [{cron: "0 1 * * *"}]
  push:
    branches: ["*"]
  pull_request:
    branches: ["*"]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - uses: docker://dkhamsing/awesome_bot:latest
        with:
          args: /github/workspace/README.md --allow-timeout --allow 202,206,403,429,500,501,502,503,504,509,521,522 --allow-dupe --allow-ssl --request-delay 1 --allow-redirect --white-list https://ipfs.io,slideshare,https://img.shields.io,https://codeclimate.com/,https://www.concourse.ci
          # args: /github/workspace/README.md --allow-timeout --allow 202,206,403,429,500,501,502,503,504,509,521,522 --allow-dupe --allow-ssl --request-delay 1 --allow-redirect --white-list https://ipfs.io,slideshare,https://img.shields.io,https://codeclimate.com/github/unixorn/awesome-zsh-plugins,www-s.acm.illinois.edu,https://mgdm.net,https://www.concourse.ci,https://grml.org/zsh/zsh-lovers.html,https://geeknote.me,https://en.ipip.net,https://docs.virtuozzo.com,kubernetes.io,https://youtube-dl.org,https://1password.com,https://iterm2.com,https://mercurial-scm.org,https://hitokoto.cn,https://www.cyberciti.biz,https://keybase.io,https://exercism.io,https://bitbucket.org,https://code.visualstudio.com,https://www.gnu.org


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to npm

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Install Perl and App::FatPacker
        run: |
          sudo apt-get update
          sudo apt-get install -y perl cpanminus
          cpanm --local-lib=~/perl5 App::FatPacker
          echo "PATH=$HOME/perl5/bin:$PATH" >> $GITHUB_ENV
          echo "PERL5LIB=$HOME/perl5/lib/perl5:$PERL5LIB" >> $GITHUB_ENV

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22.14.0
          registry-url: 'https://registry.npmjs.org'

      - name: Install and Build
        run: |
          npm ci
          npm run build

      - name: Publish to npm
        run: |
          npm install -g npm@latest
          npm publish --provenance --access public --registry https://registry.npmjs.org/


================================================
FILE: .gitignore
================================================
/dist/


================================================
FILE: .gitmodules
================================================
[submodule "test/bats"]
	path = test/bats
	url = https://github.com/bats-core/bats-core.git
[submodule "test/test_helper/bats-support"]
	path = test/test_helper/bats-support
	url = https://github.com/bats-core/bats-support.git
[submodule "test/test_helper/bats-assert"]
	path = test/test_helper/bats-assert
	url = https://github.com/bats-core/bats-assert.git


================================================
FILE: .npmignore
================================================
/test/


================================================
FILE: .travis.yml
================================================
os: linux
language: perl
perl:
  - blead
  - dev
  - 5.30
  - 5.28
  - 5.26
  - 5.24
  - 5.22
  - 5.20
  - 5.18
  - 5.14

matrix:
  include:
    - os: osx
      osx_image: xcode12.2
      language: generic
      perl: 5.30
  allow_failures:
    - perl: "blead"

addons:
  homebrew:
    packages:
      - perl
      - cpanminus
    update: true

before_install:
  - git submodule sync
  - git submodule update --init
  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
      mkdir -p ~/perl5;
    fi
  - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
      eval $(curl https://travis-perl.github.io/init) --perl;
      sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";
      sudo apt-get -y update;
    fi
install:
  - cpanm --quiet --notest --local-lib=~/perl5 local::lib
  - eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
  - cpanm --quiet --notest Test::Perl::Critic Perl::Critic::Freenode
script:
  - perlcritic -1 -q --theme freenode diff-so-fancy
  - ./test/bats/bin/bats test


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 So Fancy team

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# 🎩 diff-so-fancy  [![Circle CI build](https://circleci.com/gh/so-fancy/diff-so-fancy.svg?style=shield)](https://circleci.com/gh/so-fancy/diff-so-fancy) [![AppVeyor build](https://ci.appveyor.com/api/projects/status/github/so-fancy/diff-so-fancy?branch=master&svg=true)](https://ci.appveyor.com/project/stevemao/diff-so-fancy/branch/master)

`diff-so-fancy` makes your diffs **human** readable instead of machine readable. This helps improve code quality and helps you spot defects faster.

## 🖼️ Screenshot

Vanilla `git diff` vs `git` and `diff-so-fancy`

![diff-highlight vs diff-so-fancy](diff-so-fancy.png)

## 📦 Install

Simply copy the `diff-so-fancy` script from the latest Github release into your `$PATH` and you're done. Alternately to test development features you can clone this repo and then put the `diff-so-fancy` script (symlink will work) into your `$PATH`. The `lib/` directory will need to be kept relative to the core script.

`diff-so-fancy` is also available from the [NPM registry](https://www.npmjs.com/package/diff-so-fancy), [brew](https://formulae.brew.sh/formula/diff-so-fancy), [Fedora](https://packages.fedoraproject.org/pkgs/diff-so-fancy/diff-so-fancy/), in the [Arch extra repo](https://archlinux.org/packages/extra/any/diff-so-fancy/), and as [ppa:aos for Debian/Ubuntu Linux](https://github.com/aos/dsf-debian).

Issues relating to packaging ("installation does not work", "version is out of date", etc.) should be directed to those packages' repositories/issue trackers where applicable.

## ✨ Usage

### Git

Configure git to use `diff-so-fancy` for all diff output:

```shell
git config --global core.pager "diff-so-fancy | less --tabs=4 -RF"
git config --global interactive.diffFilter "diff-so-fancy --patch"
```

### Diff

Use `-u` with `diff` for unified output, and pipe the output to `diff-so-fancy`:

```shell
diff -u file_a file_b | diff-so-fancy
```

We also support recursive mode with `-r` or `--recursive`

```shell
diff --recursive -u /path/folder_a /path/folder_b | diff-so-fancy
```

## ⚒️ Options

### markEmptyLines

Colorize the first block of an empty line. (Default: true)

```shell
git config --bool --global diff-so-fancy.markEmptyLines false
```

### changeHunkIndicators

Simplify Git header chunks to a human readable format. (Default: true)

```shell
git config --bool --global diff-so-fancy.changeHunkIndicators false
```

### stripLeadingSymbols

Should the `+` or `-` symbols at line-start be removed. (Default: true)

```shell
git config --bool --global diff-so-fancy.stripLeadingSymbols false
```

### useUnicodeRuler

By default, the separator for the file header uses Unicode line-drawing characters.
If this is causing output errors on your terminal, set this to `false` to use
ASCII characters instead. (Default: true)

```shell
git config --bool --global diff-so-fancy.useUnicodeRuler false
```

### rulerWidth

By default, the separator for the file header spans the full width of the terminal.
Use rulerWidth to set the width of the file header manually.

```shell
git config --global diff-so-fancy.rulerWidth 80
```

## 👨 The diff-so-fancy team

| Person                | Role             |
| --------------------- | ---------------- |
| @scottchiefbaker      | Project lead     |
| @OJFord               | Bug triage       |
| @GenieTim             | Travis OSX fixes |
| @AOS                  | Debian packager  |
| @Stevemao/@Paul Irish | NPM release team |

## 🧬 Contributing

Pull requests are quite welcome, and should target the
[`next` branch](https://github.com/so-fancy/diff-so-fancy/tree/next). We are also
looking for any feedback or ideas on how to make `diff-so-fancy` even *fancier*.

### Other documentation

* [Pro-tips for advanced users](pro-tips.md)
* [Reporting Bugs](reporting-bugs.md)
* [Hacking and Testing](hacking-and-testing.md)
* [History](history.md)

## 🔃 Alternatives

* [Delta](https://github.com/dandavison/delta)
* [Lazygit](https://github.com/jesseduffield/lazygit) with diff-so-fancy [integration](https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff-so-fancy)

## 🏛️ License

MIT


================================================
FILE: appveyor.yml
================================================
build: false

before_test:
  - git submodule sync
  - git submodule update --init

test_script:
  - C:\cygwin\bin\bash -lc "cd $APPVEYOR_BUILD_FOLDER; ./test/bats/bin/bats test"


================================================
FILE: diff-so-fancy
================================================
#!/usr/bin/env perl

my $VERSION = "1.4.7";

#################################################################################

use v5.014;                                                 # Require Perl 5.14 for 'state' variables and /u in regexes
use warnings FATAL => 'all';
use strict;

use File::Spec;                                             # For catdir
use File::Basename;                                         # For dirname
use Cwd qw(abs_path);                                       # For realpath()
use lib dirname(abs_path(File::Spec->catdir($0))) . "/lib"; # Add the local lib/ to @INC
use DiffHighlight;

my $remove_file_add_header     = 1;
my $remove_file_delete_header  = 1;
my $clean_permission_changes   = 1;
my $patch_mode                 = 0;
my $manually_color_lines       = 0; # Usually git/hg colorizes the lines, but for raw patches we use this
my $change_hunk_indicators     = git_config_boolean("diff-so-fancy.changeHunkIndicators","true");
my $strip_leading_indicators   = git_config_boolean("diff-so-fancy.stripLeadingSymbols","true");
my $mark_empty_lines           = git_config_boolean("diff-so-fancy.markEmptyLines","true");
my $use_unicode_dash_for_ruler = git_config_boolean("diff-so-fancy.useUnicodeRuler","true");
my $ruler_width                = git_config("diff-so-fancy.rulerWidth", undef);
my $git_strip_prefix           = git_config_boolean("diff.noprefix","false");
my $has_stdin                  = has_stdin();

my $ansi_regex       = qr/\e\[([0-9]{0,3}(;[0-9]{1,3}){0,10})[mK]/;
my $ansi_color_regex = qr/(${ansi_regex})?/;
my $reset_color      = color("reset");
my $bold             = color("bold");
my $meta_color       = "";

# Set the diff highlight colors from the config
init_diff_highlight_colors();

my ($file_1,$file_2);
my $args              = argv(); # Hashref of all the ARGV stuff
my $last_file_seen    = "";
my $last_file_mode    = "";
my $i                 = 0;
my $in_hunk           = 0;
my $columns_to_remove = 0;
my $is_mercurial      = 0;
my $color_forced      = 0; # Has the color been forced on/off

if ($args->{rulerWidth}) {
	$ruler_width = int($args->{rulerWidth});
}

# We try and be smart about whether we need to do line coloring, but
# this is an option to force it on/off
if ($args->{color_on}) {
	$manually_color_lines = 1;
	$color_forced         = 1;
} elsif ($args->{color_off}) {
	$manually_color_lines = 0;
	$color_forced         = 1;
}

if ($args->{debug}) {
	show_debug_info();
}

# We only process ARGV if we don't have STDIN
if (!$has_stdin) {
	if ($args->{v} || $args->{version}) {
		die(version());
	} elsif ($args->{'set-defaults'}) {
		my $ok = set_defaults();
		exit;
	} elsif ($args->{colors}) {
		# We print this to STDOUT so we can redirect to bash to auto-set the colors
		print get_default_colors();
		exit;
	} elsif (!%$args || $args->{help} || $args->{h}) {
		die(usage());
	} else {
		die("Missing input on STDIN\n");
	}
}

#################################################################################
#################################################################################

# The logic here is that we run all the lines through DiffHighlight first. This
# highlights all the intra-word changes. Then we take those lines and send them
# to do_dsf_stuff() to convert the diff to human readable d-s-f output and add
# appropriate fanciness

my @lines;
local $DiffHighlight::line_cb = sub {
	push(@lines,@_);

	# Wait until we have enough lines to process, then divide up into safe
	# parseable chunks and have d-s-f do it's magic
	if (@lines > 100) {
		my @chunks = get_diff_chunks(\@lines);
		@lines     = ();

		foreach my $chunk (@chunks) {
			do_dsf_stuff($chunk);
		}
	}
};

my $line_count = 0;
while (my $line = <STDIN>) {
	# If the very first line of the diff doesn't start with ANSI color we're assuming
	# it's a raw patch file, and we have to color the added/removed lines ourself
	if (!$color_forced && $line_count == 0 && !starts_with_ansi($line)) {
		$manually_color_lines = 1;
	}

	my $ok = DiffHighlight::handle_line($line);
	$line_count++;
}

# If we're mid hunk above process anything still pending
DiffHighlight::flush();

# Process anything left over
my @chunks = get_diff_chunks(\@lines);
foreach my $chunk (@chunks) {
	do_dsf_stuff($chunk);
}

#################################################################################
#################################################################################

sub do_dsf_stuff {
	my $input = shift();

	# If we're in debug mode we want to log the chunks we see
	if ($args->{debug}) { log_chunks($input); }

	# FIXME: There is a scenario where there are FIVE diff header lines
	# I'm not sure if we need to account for that or not. Unit tests pass so... bees?
	my $cnt = count_git_header_lines(@$input);
	if ($cnt == 4) {
		$patch_mode = 1;
	}

	# Calculate the context lines for this chunk
	my $context_lines = calculate_context_lines(@$input);

	#print STDERR "START -------------------------------------------------\n";
	#print STDERR join("",@$input);
	#print STDERR "END ---------------------------------------------------\n";

	# We track if the last '^diff' line had recursive specified
	my $recursive = 0;

	while (my $line = shift(@$input)) {
		######################################################
		# Pre-process the line before we do any other markup #
		######################################################

		# If the first line of the input is a blank line, skip that
		if ($i == 0 && $line =~ /^\s*$/) {
			next;
		}

		######################
		# End pre-processing #
		######################

		#######################################################################

		####################################################################
		# Look for git index and replace it horizontal line (header later) #
		####################################################################
		if ($line =~ /^${ansi_color_regex}index /) {
			# Print the line color and then the actual line
			$meta_color = $1 || get_config_color("meta");

			# Get the next line without incrementing counter while loop
			my $next = $input->[0] || "";
			my ($file_1,$file_2);

			# The line immediately after the "index" line should be the --- file line
			# If it's not it's an empty file add/delete
			if ($next !~ /^$ansi_color_regex(---|Binary files)/) {

				# We fake out the file names since it's a raw add/delete
				if ($last_file_mode eq "add") {
					$file_1 = "/dev/null";
					$file_2 = $last_file_seen;
				} elsif ($last_file_mode eq "delete") {
					$file_1 = $last_file_seen;
					$file_2 = "/dev/null";
				}
			}

			if ($file_1 && $file_2) {
				print horizontal_rule($meta_color);
				print $meta_color . file_change_string($file_1,$file_2) . "\n";
				print horizontal_rule($meta_color);
			}
		#########################
		# Look for the filename #
		#########################
		#                                            $4
		} elsif ($line =~ /^${ansi_color_regex}diff (.+?)$/) {
			my $extra = $4;

			# Mercurial looks like: diff --recursive -u core/app.py language/app.py
			if ($extra =~ m/(-r|--recursive) -u (.+?) (.*)/) {
				$is_mercurial = 1;
				$meta_color   = get_config_color("meta");

				$last_file_seen = basename($3 || "");
				$recursive      = 1;
			# Git looks like: diff --git a/diff-so-fancy b/diff-so-fancy
			} elsif ($extra =~ m/--git/) {
				# Note file may contains spaces
				my ($a_file, $b_file) = $extra =~ m/(a\/.+) (b\/.+)/;
				$a_file ||= "";
				$b_file ||= "";

				$last_file_seen = $a_file;
				$recursive      = 0;
			}

			$last_file_seen =~ s|^\w/||; # Remove a/ (and handle diff.mnemonicPrefix).
			$in_hunk        = 0;

			if ($patch_mode) {
				# we are consuming one line, and the debt must be paid
				print "\n";
			}
		########################################
		# Find the first file: --- a/README.md #
		########################################
		} elsif (!$in_hunk && $line =~ /^$ansi_color_regex--- (\w\/)?(.+?)(\e|\t|$)/) {
			$meta_color = get_config_color("meta");

			if ($git_strip_prefix) {
				my $file_dir = $4 || "";
				$file_1 = $file_dir . $5;
			} else {
				$file_1 = $5;
			}

			# Find the second file on the next line: +++ b/README.md
			my $next = shift(@$input) || "";
			$next    =~ /^$ansi_color_regex\+\+\+ (\w\/)?(.+?)(\e|\t|$)/;
			if ($1) {
				print $1; # Print out whatever color we're using
			}
			if ($git_strip_prefix) {
				my $file_dir = $4 || "";
				$file_2 = $file_dir . $5;
			} else {
				$file_2 = $5;
			}

			if ($file_2 ne "/dev/null") {
				$last_file_seen = $file_2;
			}

			# In recursive mode we massage the file names so they're similar
			if ($recursive) {
				# In recursive the files have the date appended:
				# --- /var/tmp/a/index.txt        2026-03-13 21:35:20.997231861 -0700
				# so we remove the date portion
				$file_1 =~ s/\s+(\d{4}-\d{2}-\d{2}.+)//;
				$file_2 =~ s/\s+(\d{4}-\d{2}-\d{2}.+)//;

				my $common = common_prefix($file_1, $file_2);

				# Remove the common prefix from each as well as the next dir
				$file_1 =~ s/$common.+?\///;
				$file_2 =~ s/$common.+?\///;
			}

			# Print out the top horizontal line of the header
			print $reset_color;
			print horizontal_rule($meta_color);

			# Mercurial coloring is slightly different so we need to hard reset colors
			if ($is_mercurial) {
				print $reset_color;
			}

			print $meta_color;
			print file_change_string($file_1,$file_2) . "\n";

			# Print out the bottom horizontal line of the header
			print horizontal_rule($meta_color);
		########################################
		# Check for "@@ -3,41 +3,63 @@" syntax #
		########################################
		} elsif (!$change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
			$in_hunk = 1;

			print $line;
		} elsif ($change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
			$in_hunk = 1;

			my $hunk_header = $4;
			my $remain      = bleach_text($5);

			# The number of colums to remove (1 or 2) is based on how many commas in the hunk header
			$columns_to_remove   = (char_count(",",$hunk_header)) - 1;
			# On single line removes there is NO comma in the hunk so we force one
			if ($columns_to_remove <= 0) {
				$columns_to_remove = 1;
			}

			if ($1) {
				print $1; # Print out whatever color we're using
			}

			my ($orig_offset, $orig_count, $new_offset, $new_count) = parse_hunk_header($hunk_header);
			#$last_file_seen = basename($last_file_seen);

			# Figure out the start line
			my $start_line = start_line_calc($new_offset, $new_count, $context_lines);

			# Last function has it's own color
			my $last_function_color = "";
			if ($remain) {
				$last_function_color = get_config_color("last_function");
			}

			# Check to see if we have the color for the fragment from git
			if ($5 =~ /\e\[\d/) {
				#print "Has ANSI color for fragment\n";
			} else {
				# We don't have the ANSI sequence so we shell out to get it
				#print "No ANSI color for fragment\n";
				my $frag_color = get_config_color("fragment");
				print $frag_color;
			}

			print "@ $last_file_seen:$start_line \@${reset_color}${last_function_color}${remain}${reset_color}\n";
		###################################
		# Remove any new file permissions #
		###################################
		} elsif ($remove_file_add_header && $line =~ /^${ansi_color_regex}new file mode [0-7]{6}/) {
			# Don't print the line (i.e. remove it from the output);
			$last_file_mode = "add";
			if ($patch_mode) {
				print "\n";
			}
		######################################
		# Remove any delete file permissions #
		######################################
		} elsif ($remove_file_delete_header && $line =~ /^${ansi_color_regex}deleted file mode [0-7]{6}/) {
			# Don't print the line (i.e. remove it from the output);
			$last_file_mode = "delete";
			if ($patch_mode) {
				print "\n";
			}
		################################
		# Look for binary file changes #
		################################
		} elsif ($line =~ /^Binary files (\w\/)?(.+?) and (\w\/)?(.+?) differ/) {
			my $change = file_change_string($2,$4);
			print horizontal_rule($meta_color);
			print "$meta_color$change (binary)\n";
			print horizontal_rule($meta_color);
		#####################################################
		# Check if we're changing the permissions of a file #
		#####################################################
		} elsif ($clean_permission_changes && $line =~ /^${ansi_color_regex}old mode (\d+)/) {
			my ($old_mode) = $4;
			my $next = shift(@$input);

			if ($1) {
				print $1; # Print out whatever color we're using
			}

			my ($new_mode) = $next =~ m/new mode (\d+)/;

			if ($patch_mode) {
				print "\n";
			}
			print "$last_file_seen changed file mode from $old_mode to $new_mode\n";

		###############
		# File rename #
		###############
		} elsif ($line =~ /^${ansi_color_regex}similarity index (\d+)%/) {
			my $simil = $4;

			# If it's a move with content change we ignore this and the next two lines
			if ($simil != 100) {
				shift(@$input);
				shift(@$input);
				next;
			}

			my $next    = shift(@$input);
			my ($action1, $file1) = $next =~ /(copy|rename) from (.+?)(\e|\t|$)/;

			$next       = shift(@$input);
			my ($action2, $file2) = $next =~ /(copy|rename) to (.+?)(\e|\t|$)/;

			if ($file1 && $file2) {
				# We may not have extracted this yet, so we pull from the config if not
				$meta_color = get_config_color("meta");

				my $change = "???";
				if ($action1 eq "copy") {
					$change = "Copied $file1 to $file2";
				} else {
					$change = file_change_string($file1,$file2);
				}

				print horizontal_rule($meta_color);
				print $meta_color . $change . "\n";
				print horizontal_rule($meta_color);
			}

			$i += 3; # We've consumed three lines
			next;
		#####################################
		# Just a regular line, print it out #
		#####################################
		} else {
			# Mark empty line with a red/green box indicating addition/removal
			if ($mark_empty_lines) {
				$line = mark_empty_line($line);
			}

			# Remove the correct number of leading " " or "+" or "-"
			if ($strip_leading_indicators) {
				$line = strip_leading_indicators($line,$columns_to_remove);
			}
			print $line;
		}

		$i++;
	}
}

######################################################################################################
# End regular code, begin functions
######################################################################################################

# Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805
sub parse_hunk_header {
	my ($line) = @_;
	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = $line =~ /^\@\@+(?: -(\d+)(?:,(\d+))?)+ \+(\d+)(?:,(\d+))? \@\@+/;
	$o_cnt = 1 unless defined $o_cnt;
	$n_cnt = 1 unless defined $n_cnt;
	return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
}

# Mark the first char of an empty line
sub mark_empty_line {
	my $line = shift();

	my $reset_color  = "\e\\[0?m";
	my $reset_escape = "\e\[m";
	my $invert_color = "\e\[7m";
	my $add_color    = $DiffHighlight::NEW_HIGHLIGHT[1];
	my $del_color    = $DiffHighlight::OLD_HIGHLIGHT[1];

	# This captures lines that do not have any ANSI in them (raw vanilla diff)
	if ($line eq "+\n") {
		$line = $invert_color . $add_color . " " . color('reset') . "\n";
	# This captures lines that do not have any ANSI in them (raw vanilla diff)
	} elsif ($line eq "-\n") {
		$line = $invert_color . $del_color . " " . color('reset') . "\n";
	# This handles everything else
	} else {
		$line =~ s/^($ansi_color_regex)[+-]$reset_color\s*$/$invert_color$1 $reset_escape\n/;
	}

	return $line;
}

# String to boolean
sub boolean {
	my $str = shift();
	$str    = trim($str);

	if ($str eq "" || $str =~ /^(no|false|0)$/i) {
		return 0;
	} else {
		return 1;
	}
}

# Get the git config
sub git_config_raw {
	my $cmd = "git config --list 2>&1";
	my @out = `$cmd`;

	return \@out;
}

# Memoize fetching a textual item from the git config
sub git_config {
	my $search_key    = lc($_[0] || "");
	my $default_value = lc($_[1] || "");

	state $raw = {};
	if (%$raw && $search_key) {
		return $raw->{$search_key} || $default_value;
	}

	if ($args->{debug}) {
		print "Parsing git config\n";
	}

	my $out = git_config_raw();

	foreach my $line (@$out) {
		if ($line =~ /=/) {
			my ($key,$value) = split("=",$line,2);
			$value =~ s/\s+$//;
			$raw->{$key} = $value;
		}
	}

	# If we're given a search key return that, else return the hash
	if ($search_key) {
		return $raw->{$search_key} || $default_value;
	} else {
		return $raw;
	}
}

# Fetch a boolean item from the git config
sub git_config_boolean {
	my $search_key    = lc($_[0] || "");
	my $default_value = lc($_[1] || 0); # Default to false

	my $result = git_config($search_key,$default_value);
	my $ret    = boolean($result);

	return $ret;
}

sub get_less_charset {
	my @less_char_vars = ("LESSCHARSET", "LESSCHARDEF", "LC_ALL", "LC_CTYPE", "LANG");
	foreach my $key (@less_char_vars) {
		my $val = $ENV{$key};

		if (defined $val) {
			return ($key, $val);
		}
	}

	return ();
}

sub should_print_unicode {
	if (-t STDOUT) {
		# Always print unicode chars if we're not piping stuff, e.g. to less(1)
		return 1;
	}

	# Otherwise, assume we're piping to less(1)
	my ($less_env_var, $less_charset) = get_less_charset();
	if ($less_charset && $less_charset =~ /utf-?8/i) {
		return 1;
	}

	return 0;
}

# Try and be smart about what line the diff hunk starts on
sub start_line_calc {
	my ($line_num, $diff_context, $context_lines) = @_;
	my $ret;

	if ($line_num == 0 && $diff_context == 0) {
		return 1;
	}

	# Three lines on either side, and the line itself = 7
	my $expected_context = ($context_lines * 2 + 1);

	# The first three lines
	if ($line_num == 1 && $diff_context < $expected_context) {
		$ret = $diff_context - $context_lines;
	} else {
		$ret = $line_num + $context_lines;
	}

	if ($ret < 1) {
		$ret = 1;
	}

	return $ret;
}

# Remove + or - at the beginning of the lines
sub strip_leading_indicators {
	my $line              = shift(); # Array passed in by reference
	my $columns_to_remove = shift(); # Don't remove any lines by default

	if ($columns_to_remove == 0) {
		return $line; # Nothing to do
	}

	$line =~ s/^(${ansi_color_regex})([ +-]){${columns_to_remove}}/$1/;

	if ($manually_color_lines) {
		if (defined($5) && $5 eq "+") {
			my $add_line_color = get_config_color("add_line");
			$line              = $add_line_color . insert_reset_at_line_end($line);
		} elsif (defined($5) && $5 eq "-") {
			my $remove_line_color = get_config_color("remove_line");
			$line                 = $remove_line_color . insert_reset_at_line_end($line);
		}
	}

	return $line;
}

# Insert the color reset code at end of line, but before any newlines
sub insert_reset_at_line_end {
	my $line = shift();
	$line =~ s/^(.*)([\n\r]+)?$/${1}${reset_color}${2}/;
	return $line;
}

# Count the number of a given char in a string
# https://www.perturb.org/display/1010_Perl_Count_occurrences_of_substring.html
sub char_count {
	my ($needle, $haystack) = @_;

	my $count = () = ($haystack =~ /$needle/g);

	return $count;
}

# Remove all ANSI codes from a string
sub bleach_text {
	my $str = shift();
	$str    =~ s/\e\[\d*(;\d+)*m//mg;

	return $str;
}

# Remove all trailing and leading spaces
sub trim {
	my $s = shift();
	if (!$s) { return ""; }

	$s =~ s/^\s*//u;
	$s =~ s/\s*$//u;

	return $s;
}

# Print a line of em-dash or line-drawing chars the full width of the screen
sub horizontal_rule {
	my $color = $_[0] || "";
	my $width = get_terminal_width();

	# em-dash http://www.fileformat.info/info/unicode/char/2014/index.htm
	#my $dash = "\x{2014}";
	# BOX DRAWINGS LIGHT HORIZONTAL http://www.fileformat.info/info/unicode/char/2500/index.htm
	my $dash;
	if ($use_unicode_dash_for_ruler && should_print_unicode()) {
		#$dash = Encode::encode('UTF-8', "\x{2500}");
		$dash = "\xE2\x94\x80";
	} else {
		$dash = "-";
	}

	# Draw the line
	my $ret = $color . ($dash x $width) . "$reset_color\n";

	return $ret;
}

sub file_change_string {
	my $file_1 = shift();
	my $file_2 = shift();

	# If they're the same it's a modify
	if ($file_1 eq $file_2) {
		return "modified: $file_1";
	# If the first is /dev/null it's a new file
	} elsif ($file_1 eq "/dev/null") {
		my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1];
		return "added: $add_color$file_2$reset_color";
	# If the second is /dev/null it's a deletion
	} elsif ($file_2 eq "/dev/null") {
		my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1];
		return "deleted: $del_color$file_1$reset_color";
	# If the files aren't the same it's a rename
	} elsif ($file_1 ne $file_2) {
		my ($old, $new) = DiffHighlight::highlight_pair($file_1,$file_2,{only_diff => 1});
		# highlight_pair already includes reset_color, but adds newline characters that need to be trimmed off
		$old = trim($old);
		$new = trim($new);
		return "renamed: $old$meta_color to $new"
	# Something we haven't thought of yet
	} else {
		return "$file_1 -> $file_2";
	}
}

# Check to see if STDIN is connected to an interactive terminal
sub has_stdin {
	my $i   = -t STDIN;
	my $ret = int(!$i);

	return $ret;
}

# We use this instead of Getopt::Long because it's faster and we're not parsing any
# crazy arguments
# Borrowed from: https://www.perturb.org/display/1153_Perl_Quick_extract_variables_from_ARGV.html
sub argv {
	my $ret = {};

	for (my $i = 0; $i < scalar(@ARGV); $i++) {

		# If the item starts with "-" it's a key
		if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_-]*\w)$/) && ($ARGV[$i] !~ /^-\w\w/)) {
			# If the next item does not start with "--" it's the value for this item
			if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
				$ret->{$key} = $ARGV[$i + 1];
			# Bareword like --verbose with no options
			} else {
				$ret->{$key}++;
			}
		}
	}

	# We're looking for a certain item
	if ($_[0]) { return $ret->{$_[0]}; }

	return $ret;
}

# Output the command line usage for d-s-f
sub usage {
	my $out = color("white_bold") . version() . color("reset") . "\n";

	$out .= "Usage:

git diff --color | diff-so-fancy         # Use d-s-f on one diff
cat diff.txt | diff-so-fancy             # Use d-s-f on a diff/patch file
diff -u one.txt two.txt | diff-so-fancy  # Use d-s-f on unified diff output

diff-so-fancy --colors                   # View the commands to set the recommended colors
diff-so-fancy --set-defaults             # Configure git-diff to use diff-so-fancy and suggested colors
diff-so-fancy --patch                    # Use diff-so-fancy in patch mode (interoperable with `git add --patch`)

# Configure git to use d-s-f for *all* diff operations
git config --global core.pager \"diff-so-fancy | less --tabs=4 -RFX\"

# Configure git to use d-s-f for `git add --patch`
git config --global interactive.diffFilter \"diff-so-fancy --patch\"\n";

	return $out;
}

sub get_default_colors {
	my $out  = "# Recommended default colors for diff-so-fancy\n";
	$out    .= "# --------------------------------------------\n";
	$out    .= 'git config --global color.ui true

git config --global color.diff-highlight.oldNormal    "red bold"
git config --global color.diff-highlight.oldHighlight "red bold 52"
git config --global color.diff-highlight.newNormal    "green bold"
git config --global color.diff-highlight.newHighlight "green bold 22"

git config --global color.diff.meta       "yellow"
git config --global color.diff.frag       "magenta bold"
git config --global color.diff.commit     "yellow bold"
git config --global color.diff.old        "red bold"
git config --global color.diff.new        "green bold"
git config --global color.diff.whitespace "red reverse"
';

	return $out;
}

# Output the current version string
sub version {
	my $ret  = "Diff-so-fancy: https://github.com/so-fancy/diff-so-fancy\n";
	$ret    .= "Version      : $VERSION\n";

	return $ret;
}

sub is_windows {
	if ($^O eq 'MSWin32' or $^O eq 'dos' or $^O eq 'os2' or $^O eq 'cygwin' or $^O eq 'msys') {
		return 1;
	} else {
		return 0;
	}
}

sub set_defaults {
	my $color_config = get_default_colors();
	my $git_config   = 'git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"';
	my $first_cmd    = 'git config --global diff-so-fancy.first-run false';

	my @cmds = split(/\n/,$color_config);
	push(@cmds,$git_config);
	push(@cmds,$first_cmd);

	# Remove all comments from the commands
	foreach my $x (@cmds) {
		$x =~ s/#.*//g;
	}

	# Remove any empty commands
	@cmds = grep($_,@cmds);

	foreach my $cmd (@cmds) {
		system($cmd);
		my $exit = ($? >> 8);

		if ($exit != 0) {
			die("Error running: '$cmd' (error #18941)\n");
		}
	}

	return 1;
}

# Borrowed from: https://www.perturb.org/display/1167_Perl_ANSI_colors.html
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
	my ($str, $txt) = @_;

	# If we're NOT connected to a an interactive terminal don't do color
	#if (-t STDOUT == 0) { return ''; }

	# No string sent in, so we just reset
	if (!length($str) || $str eq 'reset') { return "\e[0m"; }

	# Some predefined colors
	my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
	$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;

	# Get foreground/background and any commands
	my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
	my ($bc)      = $str =~ /on_(\d{1,3})$/g;

	# Some predefined commands
	my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
	my $cmd_num = $cmd_map{$cmd // 0};

	my $ret = '';
	if ($cmd_num)     { $ret .= "\e[${cmd_num}m"; }
	if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
	if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
	if ($txt)         { $ret .= $txt . "\e[0m";   }

	return $ret;
}

# Get colors used for various output sections (memoized)
sub get_config_color {
	my $str = shift();

	state $static_config;

	my $ret = "";
	if ($static_config->{$str}) {
		return $static_config->{$str};
	}

	#print color(15) . "Shelling out for color: '$str'\n" . color('reset');

	if ($str eq "meta") {
		# Default ANSI yellow
		$ret = git_ansi_color(git_config('color.diff.meta')) || color(11);
	} elsif ($str eq "reset") {
		$ret = color("reset");
	} elsif ($str eq "add_line") {
		# Default ANSI green
		$ret = git_ansi_color(git_config('color.diff.new')) || color("2_bold");
	} elsif ($str eq "remove_line") {
		# Default ANSI red
		$ret = git_ansi_color(git_config('color.diff.old')) || color("1_bold");
	} elsif ($str eq "fragment") {
		$ret = git_ansi_color(git_config('color.diff.frag')) || color("13_bold");
	} elsif ($str eq "last_function") {
		$ret = git_ansi_color(git_config('color.diff.func')) || color("146_bold");
	}

	# Cache (memoize) the entry for later
	$static_config->{$str} = $ret;

	return $ret;
}

# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
sub git_ansi_color {
	my $str   = shift();
	my @parts = split(' ', $str);

	if (!@parts) {
		return '';
	}

	my $colors = {
		'black'   => 0,
		'red'     => 1,
		'green'   => 2,
		'yellow'  => 3,
		'blue'    => 4,
		'magenta' => 5,
		'cyan'    => 6,
		'white'   => 7,
		'default' => 9, # pseudo color (39/49 = set default)
		'normal'  => -1, # placeholder color to be ignored
	};

	# Bright colors are just offsets from the "regular" color
	for my $k (keys %{ $colors }) {
		$colors->{"bright" . $k} = $colors->{$k} + 60;
	}

	my @ansi_part = ();

	if (grep { /^bold$/ } @parts) {
		push(@ansi_part, "1");
	}

	if (grep { /^dim$/ } @parts) {
		push(@ansi_part, "2");
	}

	if (grep { /^ul$/ } @parts) {
		push(@ansi_part, "4");
	}

	if (grep { /^reverse$/ } @parts) {
		push(@ansi_part, "7");
	}

	# Remove parts that aren't colors
	@parts = grep { exists $colors->{$_} || is_numeric($_) || /^\#/ } @parts;

	my $fg  = $parts[0] // "";
	my $bg  = $parts[1] // "";

	set_ansi_color($fg, 0 , \@ansi_part, $colors) if $fg;
	set_ansi_color($bg, 10, \@ansi_part, $colors) if $bg;

	#############################################

	my $ansi_str = join(";", @ansi_part);
	my $ret      = "\e[" . $ansi_str . "m";

	return $ret;
}

sub set_ansi_color {
	my ($color, $increment, $ansi_part, $colors) = @_;

	my $base_code  = 30 + $increment;
	my $base8_code = 38 + $increment;
	my $ext_code   = 82 + $increment;

	if (is_numeric($color)) {
		if ($color < 8) {
			push(@$ansi_part, $color + $base_code);
		} elsif ($color < 16) {
			push(@$ansi_part, $color + $ext_code);
		} else {
			push(@$ansi_part, "$base8_code;5;$color");
		}
	# It's a full rgb code
	} elsif ($color =~ /^#/) {
		my ($rgbr, $rgbg, $rgbb) = $color =~ /.(..)(..)(..)/;
		push(@$ansi_part, "$base8_code;2;" . hex($rgbr) . ";" . hex($rgbg) . ";" . hex($rgbb));
	# It's a simple 16 color OG ansi
	} elsif ($color ne "normal") {
		my $color_num = $colors->{$color} + $base_code;
		push(@$ansi_part, $color_num);
	}
}

# Is the string 100% numeric
sub is_numeric {
	my $s = shift();

	if ($s =~ /^\d+$/) {
		return 1;
	}

	return 0;
}

# Does the string start with ANSI
sub starts_with_ansi {
	my $str = shift();

	# NOTE: This is not `ansi_color_regex`, which includes "no ANSI sequences".
	if ($str =~ /^$ansi_regex/) {
		return 1;
	} else {
		return 0;
	}
}

sub get_terminal_width {
	# Make width static so we only calculate it once
	state $width;

	if ($width) {
		return $width;
	}

	# If there is a ruler width in the config we use that
	if ($ruler_width) {
		$width = $ruler_width;
	# Otherwise we check the terminal width using tput
	} else {
		my $tput = `tput cols`;

		if ($tput) {
			$width = int($tput);

			if (is_windows()) {
				$width--;
			}
		} else {
			print color('orange') . "Warning: `tput cols` did not return numeric input" . color('reset') . "\n";
			$width = 80;
		}
	}

	return $width;
}

sub show_debug_info {
	my @less    = get_less_charset();
	my $git_ver = trim(`git --version 2>&1`);
	$git_ver    =~ s/[^\d.]//g;

	if ($git_ver !~ /git/) {
		$git_ver = "Unknown";
	} else {
		$git_ver = "v" . $git_ver;
	}

	my $out .= "Diff-so-fancy   : v$VERSION\n";
	$out    .= "Git             : $git_ver\n";
	$out    .= "Perl            : $^V\n";
	$out    .= "\n";
	$out    .= "Terminal width  : " . get_terminal_width() . "\n";
	$out    .= "Terminal \$LANG  : " . ($ENV{LANG} || "") . "\n";
	$out    .= "\n";
	$out    .= "Supports Unicode: " . yes_no(should_print_unicode()) . "\n";
	$out    .= "Unicode Ruler   : " . yes_no($use_unicode_dash_for_ruler) . "\n";
	$out    .= "\n";
	$out    .= "Less Charset Var: " . ($less[0] // "") . "\n";
	$out    .= "Less Charset    : " . ($less[1] // "") . "\n";
	$out    .= "\n";
	$out    .= "Is Windows      : " . yes_no(is_windows()) . "\n";
	$out    .= "Operating System: $^O\n";

	print $out;

	my @lines = split(/\n/, $out);
	debug_log(@lines);
}

# Boolean to yes/no string
sub yes_no {
	my $val = shift();

	if ($val) {
		return "Yes";
	} else {
		return "No";
	}
}

# If there are colors set in the gitconfig use those, otherwise leave the defaults
sub init_diff_highlight_colors {
	$DiffHighlight::NEW_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.newnormal'))    || $DiffHighlight::NEW_HIGHLIGHT[0];
	$DiffHighlight::NEW_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.newhighlight')) || $DiffHighlight::NEW_HIGHLIGHT[1];
	$DiffHighlight::NEW_HIGHLIGHT[2] = git_ansi_color(git_config('color.diff-highlight.newreset'))     || "\e[0m";

	$DiffHighlight::OLD_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.oldnormal'))    || $DiffHighlight::OLD_HIGHLIGHT[0];
	$DiffHighlight::OLD_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.oldhighlight')) || $DiffHighlight::OLD_HIGHLIGHT[1];
	$DiffHighlight::NEW_HIGHLIGHT[2] = git_ansi_color(git_config('color.diff-highlight.oldreset'))     || "\e[0m";
}

# Human readable datetime string with milliseconds
sub date_str {
	use Time::HiRes qw(gettimeofday);

	my ($sec, $usec) = gettimeofday();
	my @t            = localtime($sec);

	my $ret = sprintf(
        "%04d-%02d-%02d %02d:%02d:%02d.%03d",
        $t[5] + 1900,
        $t[4] + 1,
        $t[3],
        $t[2],
        $t[1],
        $t[0],
        int($usec / 1000),
    );

	return $ret;
}

# Write a line to the debug log which is opened on the fly as-needed
# Usage: debug_log($str) or debug_log(@lines)
sub debug_log {
	my @lines = @_;
	my $file     = "/tmp/diff-so-fancy.debug.log";
	my $date_str = date_str();

	state $fh;
	if (!$fh) {
		printf("%sDebug log enabled:%s $file\n", color('orange'), color());
		open ($fh, ">", $file) or die("Cannot write to $file");
	}

	foreach my $log_line (@lines) {
		$log_line = trim($log_line);
		print $fh "$date_str: $log_line\n";
	}

	return 1;
}

# Find the commom prefix chars between two strings
# common_prefix("foobar", "food is yummy"); # 'foo'
sub common_prefix {
	my ($a, $b) = @_;
	my $len = length($a) < length($b) ? length($a) : length($b);

	my $i = 0;
	$i++ while $i < $len && substr($a,$i,1) eq substr($b,$i,1);

	return substr($a, 0, $i);
}

# Count the number of context lines in the diff
sub calculate_context_lines {
	my @lines     = @_;
	my $count     = 0;
	my $hunk_line = 0;

	# Count the number of lines between the hunk line and the
	# first + or - line
	foreach my $line (@lines) {
		# Look for the hunk line before we start
		if ($line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
			$hunk_line = $count;
		} elsif ($hunk_line && $line =~ /^${ansi_color_regex}[+-]/) {
			my $diff = $count - $hunk_line - 1;
			#print "Hunk: $hunk_line, ChangedLine: $count ($diff context)\n";
			return $diff;
		}

		$count++;
	};

	# If for some reason we can't figure it out, assume 3
	return 3;
}

# This function divides Git/Diff strings into smaller chunks that d-s-f can
# process. The special sauce is not splitting on the Git headers, or any of
# the lines that require context awareness of the next two lines
sub get_diff_chunks {
	my ($line_ref) = @_;

	my @lines       = @$line_ref;
	my @end_lines   = (); # Array of line numbers of the END of each chunk
	my $total_lines = scalar(@lines);

	# Loop through all the lines and build end line numbers for each "chunk"
	for (my $i = 0; $i < $total_lines; $i++) {
		my $line = $lines[$i];

		if (($i > 0) && $line =~ /^${ansi_color_regex}diff/) {
			push(@end_lines, $i - 1);
		}
	}

	# The final end point is the last line
	my $last_idx = $end_lines[-1] || 0;
	if ($last_idx != ($total_lines - 1)) {
		push(@end_lines, $total_lines - 1);
	}

	# Now that we know where all the end points are we divide the big array
	# into smaller chunks
	my @ret  = ();
	my $prev = 0;
	foreach my $idx (@end_lines) {
		if ($idx > 0) {
			# Array splice the lines into a smaller chuck and put them in @ret
			my @tmp = @lines[$prev .. $idx];
			push(@ret, \@tmp);
			$prev = $idx + 1;
		}
	}

	return @ret;
}

# Log each chunk of diff for debugging purposes
sub log_chunks {
	my $line_ref = shift();

	debug_log("-------------- Chunk --------------");
	debug_log(@$line_ref);
}

# In patch mode we have to keep the line ratio 1:1 so we need to know how many
# header lines there are. Usually three or four.
sub count_git_header_lines {
	my @lines = @_;
	my $ret   = 0;

	# If we're not in --patch mode then bail out early
	if (!$args->{patch} && !$ENV{DSF_PATCH_MODE}) {
		return 0;
	}

	# Textual diff should be four lines of header
	#diff --git a/diff-so-fancy b/diff-so-fancy
	#index d952098..424d8a3 100755
	#--- a/diff-so-fancy
	#+++ b/diff-so-fancy

	# Binary diff should be three lines of header
	#diff --git a/file.txt b/file.txt
	#new file mode 100644
	#index 0000000..e69de29

	#diff --git a/hello.txt b/hello.txt
	#new file mode 100644
	#index 0000000..0c767bc
	#--- /dev/null
	#+++ b/hello.txt

	my $count = 1;
	foreach my $line (@lines) {
		if ($line =~ /^${ansi_color_regex}index/) {
			last;
		}

		$count++;
	}

	if ($count == 2) {
		$count = 4;
	}

	$ret = $count;

	# No Git/Diff header should be more than four lines
	if ($ret > 4) {
		$ret = 0;
	}

	return $ret;
}

# Borrowed from: https://www.perturb.org/display/1097_Perl_detect_if_a_module_is_installed_before_using_it.html
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
	if (!defined(&trim)) {
		*trim = sub {
			my ($s) = (@_, $_); # Passed in var, or default to $_
			if (length($s) == 0) { return ""; }
			$s =~ s/^\s*//;
			$s =~ s/\s*$//;

			return $s;
		}
	}

	if ($ENV{USER} eq "bakers") {
		require Dump::Krumo;
		Dump::Krumo->import(qw(k kd));
	}
}

################################################################################

=pod

=encoding utf8

=head1 NAME

diff-so-fancy makes your diffs human readable instead of machine readable. This
helps improve code quality and helps you spot defects faster.

=head1 USAGE

=head2 Git

Configure git to use C<diff-so-fancy> for all diff output:

    git config --global core.pager "diff-so-fancy | less --tabs=4 -RF"
    git config --global interactive.diffFilter "diff-so-fancy --patch"

=head2 Diff

Use C<-u> with diff for unified output, and pipe the output to C<diff-so-fancy>:

    diff -u file_a file_b | diff-so-fancy

We also support recursive mode with C<-r> or C<--recursive>

    diff --recursive -u /path/folder_a /path/folder_b | diff-so-fancy

=head1 OPTIONS

B<markEmptyLines:> Colorize the first block of an empty line. (Default: true)

    git config --bool --global diff-so-fancy.markEmptyLines false

B<changeHunkIndicators:> Simplify Git header chunks to a human readable format. (Default: true)

    git config --bool --global diff-so-fancy.changeHunkIndicators false

B<stripLeadingSymbols:> Should the + or - symbols at line-start be removed. (Default: true)

    git config --bool --global diff-so-fancy.stripLeadingSymbols false

B<useUnicodeRuler:> By default, the separator for the file header uses Unicode line-drawing
characters. If this is causing output errors on your terminal, set this to false to use ASCII
characters instead. (Default: true)

    git config --bool --global diff-so-fancy.useUnicodeRuler false

B<rulerWidth:> By default, the separator for the file header spans the full width of the
terminal. Use rulerWidth to set the width of the file header manually.

    git config --global diff-so-fancy.rulerWidth 80

=head1 HOMEPAGE

- https://github.com/so-fancy/diff-so-fancy

=head1 SEE ALSO

=head2 Delta

- https://github.com/dandavison/delta

=head2 Lazygit with diff-so-fancy integration

- https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff-so-fancy

# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4


================================================
FILE: diff-so-fancy.plugin.zsh
================================================
# Add our plugin diretory to user's path
#
# See following web page for explanation of the line "ZERO=...":
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html

0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
0="${${(M)0:#/*}:-$PWD/$0}"
local diff_so_fancy_bin="${0:h}"

if [[ -z "${path[(r)${diff_so_fancy_bin}]}" ]]; then
    path+=( "${diff_so_fancy_bin}" )
fi

================================================
FILE: docs/diff-so-fancy.1
================================================
.\" -*- mode: troff; coding: utf-8 -*-
.\" Automatically generated by Pod::Man 5.0102 (Pod::Simple 3.45)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>.
.ie n \{\
.    ds C` ""
.    ds C' ""
'br\}
.el\{\
.    ds C`
.    ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el       .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD.  Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
.    if \nF \{\
.        de IX
.        tm Index:\\$1\t\\n%\t"\\$2"
..
.        if !\nF==2 \{\
.            nr % 0
.            nr F 2
.        \}
.    \}
.\}
.rr rF
.\" ========================================================================
.\"
.IX Title "DIFF-SO-FANCY 1"
.TH DIFF-SO-FANCY 1 2026-03-18 "perl v5.40.2" "User Contributed Perl Documentation"
.\" For nroff, turn off justification.  Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
.nh
.SH NAME
diff\-so\-fancy makes your diffs human readable instead of machine readable. This
helps improve code quality and helps you spot defects faster.
.SH USAGE
.IX Header "USAGE"
.SS Git
.IX Subsection "Git"
Configure git to use \f(CW\*(C`diff\-so\-fancy\*(C'\fR for all diff output:
.PP
.Vb 2
\&    git config \-\-global core.pager "diff\-so\-fancy | less \-\-tabs=4 \-RF"
\&    git config \-\-global interactive.diffFilter "diff\-so\-fancy \-\-patch"
.Ve
.SS Diff
.IX Subsection "Diff"
Use \f(CW\*(C`\-u\*(C'\fR with diff for unified output, and pipe the output to \f(CW\*(C`diff\-so\-fancy\*(C'\fR:
.PP
.Vb 1
\&    diff \-u file_a file_b | diff\-so\-fancy
.Ve
.PP
We also support recursive mode with \f(CW\*(C`\-r\*(C'\fR or \f(CW\*(C`\-\-recursive\*(C'\fR
.PP
.Vb 1
\&    diff \-\-recursive \-u /path/folder_a /path/folder_b | diff\-so\-fancy
.Ve
.SH OPTIONS
.IX Header "OPTIONS"
\&\fBmarkEmptyLines:\fR Colorize the first block of an empty line. (Default: true)
.PP
.Vb 1
\&    git config \-\-bool \-\-global diff\-so\-fancy.markEmptyLines false
.Ve
.PP
\&\fBchangeHunkIndicators:\fR Simplify Git header chunks to a human readable format. (Default: true)
.PP
.Vb 1
\&    git config \-\-bool \-\-global diff\-so\-fancy.changeHunkIndicators false
.Ve
.PP
\&\fBstripLeadingSymbols:\fR Should the + or \- symbols at line-start be removed. (Default: true)
.PP
.Vb 1
\&    git config \-\-bool \-\-global diff\-so\-fancy.stripLeadingSymbols false
.Ve
.PP
\&\fBuseUnicodeRuler:\fR By default, the separator for the file header uses Unicode line-drawing
characters. If this is causing output errors on your terminal, set this to false to use ASCII
characters instead. (Default: true)
.PP
.Vb 1
\&    git config \-\-bool \-\-global diff\-so\-fancy.useUnicodeRuler false
.Ve
.PP
\&\fBrulerWidth:\fR By default, the separator for the file header spans the full width of the
terminal. Use rulerWidth to set the width of the file header manually.
.PP
.Vb 1
\&    git config \-\-global diff\-so\-fancy.rulerWidth 80
.Ve
.SH HOMEPAGE
.IX Header "HOMEPAGE"
\&\- https://github.com/so\-fancy/diff\-so\-fancy
.SH "SEE ALSO"
.IX Header "SEE ALSO"
.SS Delta
.IX Subsection "Delta"
\&\- https://github.com/dandavison/delta
.SS "Lazygit with diff-so-fancy integration"
.IX Subsection "Lazygit with diff-so-fancy integration"
\&\- https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff\-so\-fancy
.PP
# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4


================================================
FILE: hacking-and-testing.md
================================================
### Hacking

```sh
# fork and clone the diff-so-fancy repo.
git clone https://github.com/so-fancy/diff-so-fancy/ && cd diff-so-fancy

# test a saved diff against your local version
cat test/fixtures/ls-function.diff | ./diff-so-fancy

# setup symlinks to use local copy
npm link
cd ~/projects/catfabulator && git diff
```

### Running tests

The tests use [bats-core](https://bats-core.readthedocs.io/en/latest/index.html), the Bash automated testing system.

```sh
# initialize the bats components
git submodule sync && git submodule update --init

# run the test suite once:
./test/bats/bin/bats test

# run it on every change with `entr`
brew install entr
find  ./* test/* test/fixtures/* -maxdepth 0 | entr ./test/bats/bin/bats test
```

When writing assertions, you'll likely want to compare to expected output. To grab that reliably, you can use something like `git --no-pager diff | ./diff-so-fancy > output.txt`


### Publishing to npm

Run these one-by-one, manually.

```sh
diff-so-fancy --version # see the old version (probably)

npm run build  # build the fatpack into dist.

./dist/diff-so-fancy --version # get latest version from perl script

npm version vX.X.X   # bump package.json to match.

npm uninstall -g diff-so-fancy && npm link  # make global symlink, if not already present

diff-so-fancy --version  # ensure latest version is shown

git show | diff-so-fancy  # ensure it works

npm publish --dry-run # ensure listed files are what you want published. update .npmignore as desired

# npm login --registry https://registry.npmjs.org/  # maybe.

npm publish --registry https://registry.npmjs.org/
```



================================================
FILE: history.md
================================================
## History

`diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13349456), which grew into a [standalone script](https://github.com/paulirish/dotfiles/blob/63cb8193b0e66cf80ab6332477f1f52c7fbb9311/bin/diff-so-fancy). Later, [@stevemao](https://github.com/stevemao) brought it into its [own repo](https://github.com/so-fancy/diff-so-fancy) (here), and gave it the room to mature. It's quickly grown into a [widely collaborative project](https://github.com/so-fancy/diff-so-fancy/graphs/contributors).


================================================
FILE: lib/DiffHighlight.pm
================================================
package DiffHighlight;

use 5.008;
use warnings FATAL => 'all';
use strict;

# Use the correct value for both UNIX and Windows (/dev/null vs nul)
use File::Spec;

my $NULL = File::Spec->devnull();

# Highlight by reversing foreground and background. You could do
# other things like bold or underline if you prefer.
our @OLD_HIGHLIGHT = (
	undef,
	"\e[7m",
	"\e[27m",
);
our @NEW_HIGHLIGHT = (
	$OLD_HIGHLIGHT[0],
	$OLD_HIGHLIGHT[1],
	$OLD_HIGHLIGHT[2],
);

my $RESET = "\x1b[m";
my $COLOR = qr/\x1b\[[0-9;]*m/;
my $BORING = qr/$COLOR|\s/;

my @removed;
my @added;
my $in_hunk;
my $graph_indent = 0;

our $line_cb = sub { print @_ };
our $flush_cb = sub { local $| = 1 };

# Count the visible width of a string, excluding any terminal color sequences.
sub visible_width {
	local $_ = shift;
	my $ret = 0;
	while (length) {
		if (s/^$COLOR//) {
			# skip colors
		} elsif (s/^.//) {
			$ret++;
		}
	}
	return $ret;
}

# Return a substring of $str, omitting $len visible characters from the
# beginning, where terminal color sequences do not count as visible.
sub visible_substr {
	my ($str, $len) = @_;
	while ($len > 0) {
		if ($str =~ s/^$COLOR//) {
			next
		}
		$str =~ s/^.//;
		$len--;
	}
	return $str;
}

sub handle_line {
	my $orig = shift;
	local $_ = $orig;

	# match a graph line that begins a commit
	if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space
	         $COLOR?\*$COLOR?[ ]   # a "*" with its trailing space
	      (?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|"
	                         [ ]*  # trailing whitespace for merges
	    /x) {
		my $graph_prefix = $&;

		# We must flush before setting graph indent, since the
		# new commit may be indented differently from what we
		# queued.
		flush();
		$graph_indent = visible_width($graph_prefix);

	} elsif ($graph_indent) {
		if (length($_) < $graph_indent) {
			$graph_indent = 0;
		} else {
			$_ = visible_substr($_, $graph_indent);
		}
	}

	if (!$in_hunk) {
		$line_cb->($orig);
		$in_hunk = /^$COLOR*\@\@ /;
	}
	elsif (/^$COLOR*-/) {
		push @removed, $orig;
	}
	elsif (/^$COLOR*\+/) {
		push @added, $orig;
	}
	else {
		flush();
		$line_cb->($orig);
		$in_hunk = /^$COLOR*[\@ ]/;
	}

	# Most of the time there is enough output to keep things streaming,
	# but for something like "git log -Sfoo", you can get one early
	# commit and then many seconds of nothing. We want to show
	# that one commit as soon as possible.
	#
	# Since we can receive arbitrary input, there's no optimal
	# place to flush. Flushing on a blank line is a heuristic that
	# happens to match git-log output.
	if (/^$/) {
		$flush_cb->();
	}
}

sub flush {
	# Flush any queued hunk (this can happen when there is no trailing
	# context in the final diff of the input).
	show_hunk(\@removed, \@added);
	@removed = ();
	@added = ();
}

sub highlight_stdin {
	while (<STDIN>) {
		handle_line($_);
	}
	flush();
}

# Ideally we would feed the default as a human-readable color to
# git-config as the fallback value. But diff-highlight does
# not otherwise depend on git at all, and there are reports
# of it being used in other settings. Let's handle our own
# fallback, which means we will work even if git can't be run.
sub color_config {
	my ($key, $default) = @_;
	my $s = `git config --get-color $key 2>$NULL`;
	return length($s) ? $s : $default;
}

sub show_hunk {
	my ($a, $b) = @_;

	# If one side is empty, then there is nothing to compare or highlight.
	if (!@$a || !@$b) {
		$line_cb->(@$a, @$b);
		return;
	}

	# If we have mismatched numbers of lines on each side, we could try to
	# be clever and match up similar lines. But for now we are simple and
	# stupid, and only handle multi-line hunks that remove and add the same
	# number of lines.
	if (@$a != @$b) {
		$line_cb->(@$a, @$b);
		return;
	}

	my @queue;
	for (my $i = 0; $i < @$a; $i++) {
		my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
		$line_cb->($rm);
		push @queue, $add;
	}
	$line_cb->(@queue);
}

sub highlight_pair {
	my @a = split_line(shift);
	my @b = split_line(shift);

	# Find common prefix, taking care to skip any ansi
	# color codes.
	my $seen_plusminus;
	my ($pa, $pb) = (0, 0);
	while ($pa < @a && $pb < @b) {
		if ($a[$pa] =~ /$COLOR/) {
			$pa++;
		}
		elsif ($b[$pb] =~ /$COLOR/) {
			$pb++;
		}
		elsif ($a[$pa] eq $b[$pb]) {
			$pa++;
			$pb++;
		}
		elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
			$seen_plusminus = 1;
			$pa++;
			$pb++;
		}
		else {
			last;
		}
	}

	# Find common suffix, ignoring colors.
	my ($sa, $sb) = ($#a, $#b);
	while ($sa >= $pa && $sb >= $pb) {
		if ($a[$sa] =~ /$COLOR/) {
			$sa--;
		}
		elsif ($b[$sb] =~ /$COLOR/) {
			$sb--;
		}
		elsif ($a[$sa] eq $b[$sb]) {
			$sa--;
			$sb--;
		}
		else {
			last;
		}
	}

	if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
		return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT),
		       highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT);
	}
	else {
		return join('', @a),
		       join('', @b);
	}
}

# we split either by $COLOR or by character. This has the side effect of
# leaving in graph cruft. It works because the graph cruft does not contain "-"
# or "+"
sub split_line {
	local $_ = shift;
	return utf8::decode($_) ?
		map { utf8::encode($_); $_ }
			map { /$COLOR/ ? $_ : (split //) }
			split /($COLOR+)/ :
		map { /$COLOR/ ? $_ : (split //) }
		split /($COLOR+)/;
}

sub highlight_line {
	my ($line, $prefix, $suffix, $theme) = @_;

	my $start = join('', @{$line}[0..($prefix-1)]);
	my $mid = join('', @{$line}[$prefix..$suffix]);
	my $end = join('', @{$line}[($suffix+1)..$#$line]);

	# If we have a "normal" color specified, then take over the whole line.
	# Otherwise, we try to just manipulate the highlighted bits.
	if (defined $theme->[0]) {
		s/$COLOR//g for ($start, $mid, $end);
		chomp $end;
		chomp $start;
		return join('',
			$theme->[0], $start, $RESET,
			$theme->[1], $mid, $RESET,
			$theme->[0], $end, $RESET,
			"\n"
		);
	} else {
		return join('',
			$start,
			$theme->[1], $mid, $theme->[2],
			$end
		);
	}
}

# Pairs are interesting to highlight only if we are going to end up
# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
# is just useless noise. We can detect this by finding either a matching prefix
# or suffix (disregarding boring bits like whitespace and colorization).
sub is_pair_interesting {
	my ($a, $pa, $sa, $b, $pb, $sb) = @_;
	my $prefix_a = join('', @$a[0..($pa-1)]);
	my $prefix_b = join('', @$b[0..($pb-1)]);
	my $suffix_a = join('', @$a[($sa+1)..$#$a]);
	my $suffix_b = join('', @$b[($sb+1)..$#$b]);

	return visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ ||
	       visible_substr($prefix_b, $graph_indent) !~ /^$COLOR*\+$BORING*$/ ||
	       $suffix_a !~ /^$BORING*$/ ||
	       $suffix_b !~ /^$BORING*$/;
}


================================================
FILE: package.json
================================================
{
  "name": "diff-so-fancy",
  "version": "1.4.6",
  "description": "Good-lookin' diffs with diff-highlight and more",
  "bin": {
    "diff-so-fancy": "dist/diff-so-fancy"
  },
  "files": [
    "dist/diff-so-fancy"
  ],
  "scripts": {
    "test": "./test/bats/bin/bats test",
    "build": "mkdir -p dist && PATH=\"$HOME/perl5/bin:$PATH\" PERL5LIB=\"$HOME/perl5/lib/perl5:$PERL5LIB\" perl ./third_party/build_fatpack/build.pl --output ./dist/diff-so-fancy"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/so-fancy/diff-so-fancy"
  },
  "keywords": [
    "git",
    "diff",
    "fancy",
    "good-lookin'",
    "diff-highlight",
    "color",
    "readable",
    "highlight"
  ],
  "author": "Paul Irish",
  "license": "MIT",
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "bugs": {
    "url": "https://github.com/so-fancy/diff-so-fancy/issues"
  },
  "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"
}


================================================
FILE: pro-tips.md
================================================
# Pro-tips

## One-off fanciness or a specific diff-so-fancy alias

You can do also do a one-off:

```shell
git diff --color | diff-so-fancy
```

or configure an alias and a corresponding pager to use `diff-so-fancy`:

```shell
git config --global alias.dsf "diff --color"
git config --global pager.dsf "diff-so-fancy | less --tabs=4 -RFXS"
```

## Opting-out

Sometimes you will want to bypass diff-so-fancy. Use `--no-pager` for that:

```shell
git --no-pager diff
```

## Raw patches

As a shortcut for a 'normal' diff to save as a patch for emailing or later
application, it may be helpful to configure an alias:

```ini
[alias]
    patch = !git --no-pager diff --no-color
```

which can then be used as `git patch > changes.patch`.

## Improved colors for the highlighted bits

The default Git colors are not optimal. The colors used for the screenshot were:

```shell
git config --global color.ui true

git config --global color.diff-highlight.oldNormal    "red bold"
git config --global color.diff-highlight.oldHighlight "red bold 52"
git config --global color.diff-highlight.newNormal    "green bold"
git config --global color.diff-highlight.newHighlight "green bold 22"

git config --global color.diff.meta       "11"
git config --global color.diff.frag       "magenta bold"
git config --global color.diff.func       "146 bold"
git config --global color.diff.commit     "yellow bold"
git config --global color.diff.old        "red bold"
git config --global color.diff.new        "green bold"
git config --global color.diff.whitespace "red reverse"
```

#### Moving around in the diff

You can pre-seed your `less` pager with a search pattern so that you can move
between files with `n`/`N` keys:

```ini
[pager]
    diff = diff-so-fancy | less --tabs=4 -RFXS --pattern '^(Date|added|deleted|modified): '
```

## Zsh plugin suppport for diff-so-fancy

This project includes a `.plugin.zsh` file providing ZSH framework support, so you can use any framework that supports the ZSH plugin standard to install `diff-so-fancy` and add it to your `$PATH`. Installation with Zinit, Zplug, and Zgen:

### Install with zinit

```sh
zinit ice lucid as"program" pick"bin/git-dsf"
zinit load so-fancy/diff-so-fancy
```

### Install with zplug

```sh
zplug "so-fancy/diff-so-fancy", as:command, use:bin/git-dsf
```

### zgenom and others

```sh
zgenom load so-fancy/diff-so-fancy
```

## `hg` configuration

You can configure `hg diff` output to use `diff-so-fancy` by adding this alias
to your `hgrc` file:

```ini
[alias]
    diff = !HGPLAIN=1 $HG diff --pager=on --config pager.pager=diff-so-fancy $@
```


================================================
FILE: report-bug.sh
================================================
#!/bin/bash

clipboard() {
	local copy_cmd

	if [ -n "$PBCOPY_SERVER" ]; then
		local body="" # buffer
		body=$(cat)
		# while IFS= read -r buffer; do
		#   body="$body$buffer\n";
		# done
		curl "$PBCOPY_SERVER" --data-urlencode body="$body" >/dev/null 2>&1
		return $?
	fi

	if type putclip >/dev/null 2>&1; then
		copy_cmd="putclip"
	elif [ -e /dev/clipboard ];then
		cat > /dev/clipboard
		return 0
	elif type clip >/dev/null 2>&1; then
		if [[ $LANG = UTF-8 ]]; then
			copy_cmd="iconv -f utf-8 -t shift_jis | clip"
		else
			copy_cmd=clip
		fi
		# copy_cmd=clip
	elif command -v pbcopy >/dev/null 2>&1; then
		copy_cmd="pbcopy"
	elif command -v xclip >/dev/null 2>&1; then
		# copy_cmd="xclip -i -selection clipboard"
		copy_cmd="xclip"
	elif command -v xsel >/dev/null 2>&1 ; then
		local copy_cmd="xsel -b"
	fi

	if [ -n "$copy_cmd" ] ;then
		eval "$copy_cmd"
	else
		echo "clipboard is unavailable" 1>&2
	fi
}

if [ $# -eq 0 ]; then
	echo "Usage: $0 'git diff HEAD..HEAD^'"
	exit 7
fi

file=${2:-dsf-bug-report.txt}

{
	echo "$1"
	eval "$1"
	echo ""
	echo ""
	echo ""

	echo "$1 --color"
	eval "$1 --color"

	echo "git config --list | grep pager"
	eval "git config --list | grep pager"
} | base64 > "$file"

echo "Wrote file: $file"
echo "Please open a new issue on Github and attach it"


================================================
FILE: reporting-bugs.md
================================================
### Reporting bugs

If you find a bug using the following command

```sh
git diff HEAD..HEAD^
```

You can use [report-bug.sh](./report-bug.sh) we provide

```sh
./report-bug.sh 'git diff HEAD..HEAD^'
```

Attach the output file to the GitHub issue you create.


================================================
FILE: test/bugs.bats
================================================
#!/usr/bin/env bats

# Used by both `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
	load 'test_helper/bats-support/load'
	load 'test_helper/bats-assert/load'
	load 'test_helper/util'
}

setup_file() {
	__load_imports__
	setup_default_dsf_git_config
}

setup() {
	__load_imports__
}

teardown_file() {
	teardown_default_dsf_git_config
}

# https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13459061
@test "All removed lines are present in diff" {
  output=$( load_fixture "chromium-modaltoelement" | $diff_so_fancy )
	run printf "%s" "$output"

  assert_line --index 7 --partial "WebInspector.Dialog"
  assert_line --index 7 --partial "5;52m" # red oldhighlight
  assert_line --index 8 --partial "WebInspector.Dialog"
  assert_line --index 8 --partial "5;22m" # green newhighlight
}

@test "File with space in the name (#360)" {
	output=$( load_fixture "file_with_space" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 1 --regexp "added:.*a b"
}

@test "Vanilla diff with add/remove empty lines (#366)" {
	output=$( load_fixture "add_remove_empty_lines" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 5 --partial  "5;22m" # green added line
	assert_line --index 8 --partial  "5;52m" # red removed line
}

@test "recursive vanilla diff -r -bu as Mercurial (#436)" {
	output=$( load_fixture "recursive_default_as_mercurial" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 1 --partial "renamed:"
	assert_line --index 3 --partial "@ language/app.py:1 @"
	assert_line --index 19 --partial "renamed:"
	assert_line --index 21 --partial "@ language/__init__.py:1 @"
	assert_line --index 25 --partial "renamed:"
	assert_line --index 27 --partial "@ language/README.md:1 @"
}

@test "recursive vanilla diff --recursive -u as Mercurial (#436)" {
	output=$( load_fixture "recursive_longhand_as_mercurial" | $diff_so_fancy )
	run printf "%s" "$output"

    assert_output --regexp 'modified: app.py'
    assert_output --regexp 'modified: __init__.py'
    assert_output --regexp 'modified: README.md'
}

@test "Functional part with bright color (#444)" {
  output=$( load_fixture "move_with_content_change" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 3 --partial  "@ height"
}

@test "ANSI Reset without the zero (#469)" {
  output=$( load_fixture "ansi_reset_no_number" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 5 --partial  "History"
}

@test "File copy detection (#349)" {
  output=$( load_fixture "file_copy" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_output --regexp 'Copied first_file to copied_file'
}

@test "diff --recursive support (#394)" {
  output=$( load_fixture "diff_recursive" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_output --regexp 'modified: foo/bar'
  assert_output --regexp 'modified: index.txt'
}

@test "Remove a \n at the end of a file (#474)" {
  output=$( load_fixture "remove_slashn_eof" | $diff_so_fancy )
  echo $output
  run printf "%s" "$output"
  #assert_output --regexp 'one'
  assert_line --index 6 --regexp "three"
  assert_line --index 7 --regexp "three"
}


================================================
FILE: test/diff-so-fancy.bats
================================================
#!/usr/bin/env bats

# Helper invoked by `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
	load 'test_helper/bats-support/load'
	load 'test_helper/bats-assert/load'
	load 'test_helper/util'
}

setup_file() {
	__load_imports__
	set_env
	setup_default_dsf_git_config
	# bats fails to handle our multiline result, so we save to $output ourselves
	__dsf_cached_output="$( load_fixture "ls-function" | $diff_so_fancy )"
	export __dsf_cached_output
}

setup() {
	__load_imports__
	output="${__dsf_cached_output}"
}

teardown_file() {
	teardown_default_dsf_git_config
}

@test "diff-so-fancy runs and exits without error" {
	load_fixture "ls-function" | $diff_so_fancy
	run assert_success
}

@test "index line is removed entirely" {
	refute_output --partial "index 33c3d8b..fd54db2 100644"
}

@test "+/- line symbols are stripped" {
	run printf "%s" "$output"
	refute_line --index 9 --regexp "\+    set -x CLICOLOR_FORCE 1"
	refute_line --index 22 --regexp "-    eval \"env CLICOLOR"
}

@test "+/- line symbols are stripped (truecolor)" {
  output=$( load_fixture "truecolor" | $diff_so_fancy )
  refute_output --partial "
-"
  refute_output --partial "
+"
}

@test "empty lines added/removed are marked" {
	run printf "%s" "$output"

	assert_line --index 7 --partial  " "
	assert_line --index 24 --partial " "

	#assert_output --partial " "
	#assert_output --partial " "
}

@test "diff --git line is removed entirely" {
  # test against ls-function
  refute_output --partial "diff --git a/fish/functions/ls.fish"
  # test with git config diff.noprefix true
  output=$( load_fixture "noprefix" | $diff_so_fancy )
  refute_output --partial "diff --git setup-a-new-machine.sh"
}

@test "header format uses a native line-drawing character" {
  header=$( load_fixture "ls-function" | $diff_so_fancy | head -n8 )
  run printf "%s" "$header"
  assert_line --index 0 --partial "─────"
  assert_line --index 1 --partial "modified: fish/functions/ls.fish"
  assert_line --index 2 --partial "─────"
}

# see https://git.io/vrOF4
@test "Should not show unicode bytes in hex if missing LC_*/LANG _and_ piping the output" {
  unset LESSCHARSET LESSCHARDEF LC_ALL LC_CTYPE LANG
  # pipe to cat(1) so we don't open stdout
  header=$( printf "%s" "$(load_fixture "ls-function" | $diff_so_fancy | cat)" | head -n8 )
  run printf "%s" "$header"
  assert_line --index 0 --partial "-----"
  assert_line --index 1 --partial "modified: fish/functions/ls.fish"
  assert_line --index 2 --partial "-----"
  set_env # reset env
}

@test "Leading dashes are not handled as modified" {
  output=$( load_fixture "leading-dashes" | $diff_so_fancy )
  refute_output --partial "modified: Callback"
}

@test "Handle binary modifications" {
  output=$( load_fixture "binary-modified" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 1 --partial "modified: cancel.png (binary)";
}

@test "Handle unicode characters in diff output" {
  output=$( load_fixture "unicode" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 5 --partial "åäöç"
}

@test "Handle latin1 encoding sanely" {
  output=$( load_fixture "latin1" | $diff_so_fancy )
  # Make sure the output contains SOME of the english text (i.e. it doesn't barf on the whole line)
  run printf "%s" "$output"
  assert_line --index 6 --partial "saw he conqu"
}

@test "Correctly handle hunk definition with no comma" {
  output=$( load_fixture "hunk_no_comma" | $diff_so_fancy )
  # On single line removes there is NO comma in the hunk,
  # make sure the first column is still correctly stripped.
  run printf "%s" "$output"
  assert_line --index 5 --regexp "after"
}

@test "Empty file add" {
  output=$( load_fixture "add_empty_file" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 5 --regexp "added:.*empty_file.txt"
}

@test "Empty file delete" {
  output=$( load_fixture "remove_empty_file" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 5 --regexp "deleted:.*empty_file.txt"
}

@test "Move with content change" {
  output=$( load_fixture "move_with_content_change" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 1 --regexp "renamed:"
}

@test "Mercurial support" {
  output=$( load_fixture "hg" | $diff_so_fancy )
  run printf "%s" "$output"
  assert_line --index 1 --regexp "modified: hello.c"
}

@test "Handle file renames" {
	output=$( load_fixture "file-rename" | $diff_so_fancy )
	run printf "%s" "$output"
	assert_line --index 1 --partial "renamed:"
	assert_line --index 1 --partial "Changes.new"
	assert_line --index 1 --partial "bin/"
}

@test "header_clean 'added:'" {
	output=$( load_fixture "file-moves" | $diff_so_fancy )
	assert_output --regexp 'added:.*hello.txt'
}

@test "header_clean 'modified:'" {
	output=$( load_fixture "file-moves" | $diff_so_fancy )
	assert_output --partial 'modified: appveyor.yml'
}

@test "header_clean 'deleted:'" {
	output=$( load_fixture "file-moves" | $diff_so_fancy )
	assert_output --regexp 'deleted:.*circle.yml'
}

@test "header_clean permission changes" {
	output=$( load_fixture "file-perms" | $diff_so_fancy )
	assert_output --partial 'circle.yml changed file mode from 100644 to 100755'
}

@test "header_clean 'new file mode' is removed" {
	output=$( load_fixture "file-perms" | $diff_so_fancy )
	refute_output --partial 'new file mode'
}

@test "header_clean 'deleted file mode' is removed" {
	output=$( load_fixture "file-perms" | $diff_so_fancy )
	refute_output --partial 'deleted file mode'
}

@test "header_clean remove 'git --diff' header" {
	output=$( load_fixture "file-perms" | $diff_so_fancy )
	refute_output --partial 'diff --git'
}

@test "Reworked hunks" {
	output=$( load_fixture "file-moves" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 46 --partial "@ package.json:1 @"
	assert_line --index 79 --partial "@ square.yml:1 @"
}

@test "Reworked hunks (noprefix)" {
	output=$( load_fixture "noprefix" | $diff_so_fancy )
	assert_output --partial '@ setup-a-new-machine.sh:33 @'
	assert_output --partial '@ setup-a-new-machine.sh:219 @'
}

@test "Reworked hunks (deleted files)" {
	output=$( load_fixture "dotfiles" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 188 --partial "@ bin/diff-so-fancy:1 @"
}

@test "Hunk formatting: @@@ -A,B -C,D +E,F @@@" {
	# stderr forced into output
	output=$( load_fixture "complex-hunks" | $diff_so_fancy 2>&1 )
	run printf "%s" "$output"

	assert_output --regexp "@ libs/header_clean/header_clean.pl:107 @"
    refute_output --partial 'Use of uninitialized value'
}

@test "Hunk formatting: @@ -1,6 +1,6 @@" {
	# stderr forced into output
	output=$( load_fixture "first-three-line" | $diff_so_fancy )
	assert_output --partial '@ package.json:3 @'
}

@test "Hunk formatting: @@ -1 0,0 @@" {
	# stderr forced into output
	output=$( load_fixture "single-line-remove" | $diff_so_fancy )
	run printf "%s" "$output"
	assert_line --index 4 --regexp 'var delayedMessage = "It worked";'
}

@test "Three way merge" {
	# stderr forced into output
	output=$( load_fixture "complex-hunks" | $diff_so_fancy )
	# Lines should not have + or - in at the start
	refute_output --partial '-	my $foo = shift(); # Array passed in by reference'
	refute_output --partial '+	my $array = shift(); # Array passed in by reference'
	refute_output --partial ' sub parse_hunk_header {'
}

@test "mnemonicprefix handling" {
	output=$( load_fixture "mnemonicprefix" | $diff_so_fancy )
	assert_output --partial 'modified: test/header_clean.bats'
}

@test "non-git diff parsing" {
	output=$( load_fixture "weird" | $diff_so_fancy )
	run printf "%s" "$output"

	assert_line --index 1 --partial "modified: doc/manual.xml.head"
	assert_line --index 3 --partial "@ doc/manual.xml.head:8355 @"
}


================================================
FILE: test/fixtures/add_empty_file.diff
================================================
commit ef0e63dbd7e8df6cde4ee5599ad65db7820888ef
Author: Scott Baker <fake@domain.com>
Date:   Wed Jul 19 08:00:11 2017 -0700

    Add empty file

diff --git a/empty_file.txt b/empty_file.txt
new file mode 100644
index 0000000..e69de29


================================================
FILE: test/fixtures/add_remove_empty_lines.diff
================================================
--- one.txt	2020-04-23 10:15:29.193452325 -0700
+++ two.txt	2020-04-23 10:15:37.634463619 -0700
@@ -1,5 +1,5 @@
 1
+
 2
 3
-
 4


================================================
FILE: test/fixtures/ansi_reset_no_number.diff
================================================
diff --git a/history.md b/history.md
index f6776e0..a6b4546 100644
--- a/history.md
+++ b/history.md
@@ -1,3 +1,3 @@
-## History
+## Historyz
 
 `diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13349456), which grew into a [standalone script](https://github.com/paulirish/dotfiles/blob/63cb8193b0e66cf80ab6332477f1f52c7fbb9311/bin/diff-so-fancy). Later, [@stevemao](https://github.com/stevemao) brought it into its [own repo](https://github.com/so-fancy/diff-so-fancy) (here), and gave it the room to mature. It's quickly grown into a [widely collaborative project](https://github.com/so-fancy/diff-so-fancy/graphs/contributors).


================================================
FILE: test/fixtures/binary-modified.diff
================================================
diff --git a/cancel.png b/cancel.png
index 667e10c..06fc49c 100644
Binary files a/cancel.png and b/cancel.png differ
diff --git a/diff-so-fancy b/diff-so-fancy
index 0f2911c..5811717 100755
--- a/diff-so-fancy
+++ b/diff-so-fancy
@@ -32,13 +32,6 @@ color_code_regex="(${CSI}([0-9]{1,3}(;[0-9]{1,3}){0,3})[m|K])?"
 
 git_index_line_pattern="^($color_code_regex)index .*"
 
-format_diff_header () {
-  # simplify the unified patch diff header
-    $SED -E "/$git_index_line_pattern/{N;s/$git_index_line_pattern\n//;}" \
-    | $SED -E "s/^($color_code_regex)\-\-\-(.*)$/\1$(print_horizontal_rule)\\${NL}\1---\5/g" \
-    | $SED -E "s/^($color_code_regex)\+\+\+(.*)$/\1+++\5\\${NL}\1$(print_horizontal_rule)/g"
-}
-
 print_horizontal_rule () {
   let width="$(tput cols)"
 


================================================
FILE: test/fixtures/chromium-modaltoelement.diff
================================================
diff --git a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js
index 4f9adf8..8c13743 100644
--- a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js
+++ b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js
@@ -32,7 +32,7 @@
  * @constructor
  * @extends {WebInspector.Widget}
  */
-WebInspector.Dialog = function()
+WebInspector.Dialog = function(isModalToElement)
 {
     WebInspector.Widget.call(this, true);
     this.markAsRoot();
@@ -45,6 +45,9 @@ WebInspector.Dialog = function()
 
     this._wrapsContent = false;
     this._dimmed = false;
+    this._isModalToElement = isModalToElement;
+
+    this._glassPane = new WebInspector.GlassPane(relativeToElement, isModalToElement);
     /** @type {!Map<!HTMLElement, number>} */
     this._tabIndexMap = new Map();
 }
@@ -62,16 +65,16 @@ WebInspector.Dialog.prototype = {
     /**
      * @override
      */
-    show: function()
+    show: function(isModalToElement)
     {
         if (WebInspector.Dialog._instance)
             WebInspector.Dialog._instance.detach();
         WebInspector.Dialog._instance = this;
 
-        var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument);
+        var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument, isModalToElement);
         this._disableTabIndexOnElements(document);
 
-        this._glassPane = new WebInspector.GlassPane(document, this._dimmed);
+        this._glassPane = new WebInspector.GlassPane(document, isModalToElement);
         this._glassPane.element.addEventListener("click", this._onGlassPaneClick.bind(this), false);
         WebInspector.GlassPane.DefaultFocusedViewStack.push(this);
 


================================================
FILE: test/fixtures/complex-hunks.diff
================================================
commit 74804e377d4a54d1173d4393827d4e4b27e4d5d0
diff --cc libs/header_clean/header_clean.pl
index e8bcd92,5970580..ae279d0
--- a/libs/header_clean/header_clean.pl
+++ b/libs/header_clean/header_clean.pl
@@@ -105,13 -104,21 +104,23 @@@ for (my $i = 0; $i <= $#input; $i++) 
  	}
  }
  
+ # Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805
+ sub parse_hunk_header {
+     my ($line) = @_;
+     my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+         $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
+     $o_cnt = 1 unless defined $o_cnt;
+     $n_cnt = 1 unless defined $n_cnt;
+     return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
+ }
+ 
  sub strip_empty_first_line {
 -	my $foo = shift(); # Array passed in by reference
 +	my $array = shift(); # Array passed in by reference
  
  	# If the first line is just whitespace remove it
 -	if (defined($foo->[0]) && $foo->[0] =~ /^\s*$/) {
 -		shift($foo);
 +	if (defined($array->[0]) && $array->[0] =~ /^\s*$/) {
 +		shift(@$array); # Throw away the first line
  	}
 +
 +	return 1;
  }


================================================
FILE: test/fixtures/diff_recursive.diff
================================================
Only in /var/tmp/b: donk
diff --recursive -u /var/tmp/a/foo/bar /var/tmp/b/foo/bar
--- /var/tmp/a/foo/bar  2026-03-13 21:35:28.492228513 -0700
+++ /var/tmp/b/foo/bar  2026-03-13 21:35:47.101220189 -0700
@@ -1 +1 @@
-BAR
+FOO
diff --recursive -u /var/tmp/a/index.txt /var/tmp/b/index.txt
--- /var/tmp/a/index.txt        2026-03-13 21:35:20.997231861 -0700
+++ /var/tmp/b/index.txt        2026-03-13 21:35:41.030222906 -0700
@@ -1 +1,2 @@
 INDEX
+INDEX2


================================================
FILE: test/fixtures/dotfiles.diff
================================================
diff --git a/.aliases b/.aliases
index 30182ef..be9fb1d 100644
--- a/.aliases
+++ b/.aliases
@@ -18,6 +18,7 @@ alias brwe=brew  #typos
 
 alias hosts='sudo $EDITOR /etc/hosts'   # yes I occasionally 127.0.0.1 twitter.com ;)
 
+alias ag='ag -W 200 -f'
 
 ###
 # time to upgrade `ls`
@@ -51,7 +52,7 @@ alias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup
 
 # Networking. IP address, dig, DNS
 alias ip="dig +short myip.opendns.com @resolver1.opendns.com"
-alias dig="dig +nocmd any +multiline +noall +answer"
+# alias dig="dig +nocmd any +multiline +noall +answer"
 
 # Recursively delete `.DS_Store` files
 alias cleanup_dsstore="find . -name '*.DS_Store' -type f -ls -delete"
@@ -68,7 +69,13 @@ alias ungz="gunzip -k"
 alias fs="stat -f \"%z bytes\""
 
 # Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage
-alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage"
+alias emptytrash=" \ 
+    sudo rm -rfv /Volumes/*/.Trashes; \
+    rm -rfv ~/.Trash/*; \
+    sudo rm -v /private/var/vm/sleepimage; \
+    rm -rv \"/Users/paulirish/Library/Application Support/stremio/Cache\";  \
+    rm -rv \"/Users/paulirish/Library/Application Support/stremio/stremio-cache\" \
+"
 
 # Update installed Ruby gems, Homebrew, npm, and their installed packages
 alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor"
diff --git a/.bash_profile b/.bash_profile
index 8f17751..def367d 100644
--- a/.bash_profile
+++ b/.bash_profile
@@ -116,3 +116,9 @@ shopt -s cdspell;
 
 
 
+
+# The next line updates PATH for the Google Cloud SDK.
+source '/Users/paulirish/google-cloud-sdk/path.bash.inc'
+
+# The next line enables shell command completion for gcloud.
+source '/Users/paulirish/google-cloud-sdk/completion.bash.inc'
diff --git a/.bash_prompt b/.bash_prompt
index 852d69f..8d3e3d0 100644
--- a/.bash_prompt
+++ b/.bash_prompt
@@ -4,6 +4,9 @@
 default_username='paulirish'
 
 
+eval "$(thefuck --alias)"
+
+
 if [[ -n "$ZSH_VERSION" ]]; then  # quit now if in zsh
     return 1 2> /dev/null || exit 1;
 fi;
diff --git a/.bashrc b/.bashrc
index 877b68f..18461ac 100644
--- a/.bashrc
+++ b/.bashrc
@@ -1,2 +1,4 @@
 [ -n "$PS1" ] && source ~/.bash_profile
 
+
+# [ -f ~/.fzf.bash ] && source ~/.fzf.bash
diff --git a/.functions b/.functions
index 8292d2e..a548d45 100644
--- a/.functions
+++ b/.functions
@@ -28,6 +28,19 @@ cdf() {  # short for cdfinder
 }
 
 
+
+# git commit browser. needs fzf
+log() {
+  git log --graph --color=always \
+      --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" |
+  fzf --ansi --no-sort --reverse --tiebreak=index --toggle-sort=\` \
+      --bind "ctrl-m:execute:
+                echo '{}' | grep -o '[a-f0-9]\{7\}' | head -1 |
+                xargs -I % sh -c 'git show --color=always % | less -R'"
+}
+
+
+
 # Start an HTTP server from a directory, optionally specifying the port
 function server() {
 	local port="${1:-8000}"
@@ -145,17 +158,14 @@ webmify(){
 	ffmpeg -i $1 -vcodec libvpx -acodec libvorbis -isync -copyts -aq 80 -threads 3 -qmax 30 -y $2 $1.webm
 }
 
+# direct it all to /dev/null
+function nullify() {
+  "$@" >/dev/null 2>&1
+}
+
 
 # visual studio code. a la `subl`
-code () {
-	if [[ $# = 0 ]]
-	then
-		open -a "Visual Studio Code"
-	else
-		[[ $1 = /* ]] && F="$1" || F="$PWD/${1#./}"
-		open -a "Visual Studio Code" --args "$F"
-	fi
-}
+function code () { VSCODE_CWD="$PWD" open -n -b "com.microsoft.VSCode" --args $*; }
 
 # `shellswitch [bash |zsh]`
 #   Must be in /etc/shells
diff --git a/.gitconfig b/.gitconfig
index d2be05f..d32f98c 100644
--- a/.gitconfig
+++ b/.gitconfig
@@ -7,11 +7,13 @@
 	df = diff --color --color-words --abbrev
 	lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --
 	co = checkout
+	cherry = cherry-pick # does this conflict with the real `git cherry`?
 
 	# Show the diff between the latest commit and the current state
 	d = !"git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat"
 
-	reup = rebase-update
+	# chromium/depot_tools convenience. manual
+	reup = rebase-update --no_fetch --keep-going
 
 
 
@@ -23,7 +25,7 @@
 	excludesfile = ~/.gitignore
 	attributesfile = ~/.gitattributes
 	# insanely beautiful diffs
-	pager = diff-highlight | diff-so-fancy | less -r
+	pager = diff-so-fancy | less --tabs=1,5 -R
 [color "branch"]
 	current = yellow reverse
 	local = yellow
@@ -40,7 +42,8 @@
 	changed = green
 	untracked = cyan
 [merge]
-	tool = opendiff
+	#tool = opendiff
+	tool = kdiff3
 
 
 [color "diff-highlight"]
@@ -86,3 +89,5 @@
 	required = true
 [init]
 	templatedir = ~/.git_template
+[http]
+	cookiefile = /Users/paulirish/.gitcookies
diff --git a/.zshrc b/.zshrc
index 410a2ca..dbad355 100644
--- a/.zshrc
+++ b/.zshrc
@@ -113,3 +113,5 @@ source ~/.bash_profile
 
 
 export PATH="$PATH:$HOME/.rvm/bin" # Add RVM to PATH for scripting
+
+[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
diff --git a/README.md b/README.md
index 496bbbb..171eaea 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,6 @@ Lastly, I use `open .` to open Finder from this path. (That's just available nor
 ## overview of files
 
 ####  Automatic config
-* `.sift.conf` - sift (faster than grep, ack, ag)
 * `.vimrc`, `.vim` - vim config, obv.
 * `.inputrc` - behavior of the actual prompt line
 
diff --git a/bin/diff-highlight b/bin/diff-highlight
deleted file mode 120000
index 7c5c827..0000000
--- a/bin/diff-highlight
+++ /dev/null
@@ -1 +0,0 @@
-/Users/paulirish/.homebrew/share/git-core/contrib/diff-highlight/diff-highlight
\ No newline at end of file
diff --git a/bin/diff-so-fancy b/bin/diff-so-fancy
deleted file mode 100755
index 5b004a2..0000000
--- a/bin/diff-so-fancy
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/bash
-
-###############
-# diff-so-fancy builds on the good-lookin' output of diff-highlight to upgrade your diffs' appearances
-# 	* Output will not be in standard patch format, but will be readable
-#   * No pesky `+` or `-` at line-stars, making for easier copy-paste.
-#
-# Screenshot: https://github.com/paulirish/dotfiles/commit/6743b907ff58#commitcomment-13349456
-#
-#
-# Usage
-#
-#   git diff | diff-highlight | diff-so-fancy
-#
-# Add to .gitconfig so all `git diff` uses it.
-#   git config --global core.pager "diff-highlight | diff-so-fancy | less -r"
-#
-#
-# Requirements / Install
-#
-# * GNU sed. On Mac, install it with Homebrew:
-#   	brew install gnu-sed --default-names  # You'll have to change below to `gsed` otherwise
-# * diff-highlight. It's shipped with Git, but probably not in your $PATH
-#       ln -sf "$(brew --prefix)/share/git-core/contrib/diff-highlight/diff-highlight" ~/bin/diff-highlight
-# * Add some coloring to your .gitconfig:
-#		git config --global color.diff-highlight.oldNormal "red bold"
-#		git config --global color.diff-highlight.oldHighlight "red bold 52"
-#		git config --global color.diff-highlight.newNormal "green bold"
-#		git config --global color.diff-highlight.newHighlight "green bold 22"
-#
-###############
-
-# TODO:
-#   Put on NPM.
-
-
-[ $# -ge 1 -a -f "$1" ] && input="$1" || input="-"
-
-color_code_regex=$'(\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)[m|K])?'
-reset_color="\x1B\[m"
-dim_magenta="\x1B\[38;05;146m"
-
-format_diff_header () {
-	# simplify the unified patch diff header
-	sed -E "s/^($color_code_regex)diff --git .*$//g" | \
-	sed -E "s/^($color_code_regex)index .*$/\
-\1$(print_horizontal_rule)/g" | \
-	sed -E "s/^($color_code_regex)\+\+\+(.*)$/\1\+\+\+\5\\
-\1$(print_horizontal_rule)/g"
-}
-
-colorize_context_line () {
-	# extra color for @@ context line
-	sed -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g"
-}
-
-strip_leading_symbols () {
-	# strip the + and -
-	sed -E "s/^($color_code_regex)[\+\-]/\1 /g"
-}
-
-print_horizontal_rule () {
-		printf "%$(tput cols)s\n"|tr " " "─"
-}
-
-# run it.
-cat $input | format_diff_header |  colorize_context_line | strip_leading_symbols
-
-
diff --git a/brew-cask.sh b/brew-cask.sh
index 24c2ba5..3f3c02a 100755
--- a/brew-cask.sh
+++ b/brew-cask.sh
@@ -1,8 +1,8 @@
 #!/bin/bash
 
 
-# to maintain cask .... 
-#     brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup` 
+# to maintain cask ....
+#     brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup`
 
 
 # Install native apps
@@ -14,7 +14,7 @@ brew tap caskroom/versions
 brew cask install spectacle
 brew cask install dropbox
 brew cask install gyazo
-brew cask install onepassword
+brew cask install 1password
 brew cask install rescuetime
 brew cask install flux
 
@@ -47,6 +47,6 @@ brew cask install utorrent
 
 # Not on cask but I want regardless.
 
-# 3Hub   https://itunes.apple.com/us/app/3hub/id427515976?mt=12 
+# 3Hub   https://itunes.apple.com/us/app/3hub/id427515976?mt=12
 # File Multi Tool 5
 # Phosphor
\ No newline at end of file
diff --git a/brew.sh b/brew.sh
index 8715a37..c4a663e 100755
--- a/brew.sh
+++ b/brew.sh
@@ -58,7 +58,9 @@ brew install mtr
 
 
 # Install other useful binaries
-brew install sift
+brew install the_silver_searcher
+brew install fzf
+
 brew install git
 brew install imagemagick --with-webp
 brew install node # This installs `npm` too using the recommended installation method
diff --git a/fish/aliases.fish b/fish/aliases.fish
index a0fe9b3..fc990f6 100644
--- a/fish/aliases.fish
+++ b/fish/aliases.fish
@@ -26,17 +26,14 @@ alias brwe=brew  #typos
 alias hosts='sudo $EDITOR /etc/hosts'   # yes I occasionally 127.0.0.1 twitter.com ;)
 
 
-# `shellswitch [bash|zsh|fish]`
-function shellswitch
-	chsh -s (brew --prefix)/bin/$argv
-end
-
-
-
 # `cat` with beautiful colors. requires Pygments installed.
 # 							   sudo easy_install -U Pygments
 alias c='pygmentize -O style=monokai -f console256 -g'
 
+alias ag='ag -W 200 -f'
+
+alias diskspace_report="df -P -kHl"
+alias free_diskspace_report="diskspace_report"
 
 
 # Networking. IP address, dig, DNS
@@ -54,8 +51,7 @@ alias ungz="gunzip -k"
 # File size
 alias fs="stat -f \"%z bytes\""
 
-# Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage
-alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage"
+# emptytrash written as a function
 
 # Update installed Ruby gems, Homebrew, npm, and their installed packages
 alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor"
diff --git a/fish/config.fish b/fish/config.fish
index 9c4bd70..fd19027 100755
--- a/fish/config.fish
+++ b/fish/config.fish
@@ -2,9 +2,15 @@ set default_user "paulirish"
 set default_machine "paulirish-macbookair2"
 
 
+#set -x  DYLD_FALLBACK_LIBRARY_PATH /Users/paulirish/.homebrew/lib
+
 source ~/.config/fish/path.fish
 source ~/.config/fish/aliases.fish
 source ~/.config/fish/chpwd.fish
+source ~/.config/fish/functions.fish
+
+
+
 
 
 # Completions
@@ -22,8 +28,6 @@ end
 make_completion g 'git'
 
 
-
-
 # Readline colors
 set -g fish_color_autosuggestion 555 yellow
 set -g fish_color_command 5f87d7
@@ -83,3 +87,4 @@ set -gx LESS_TERMCAP_so \e'[38;5;246m'    # begin standout-mode - info box
 set -gx LESS_TERMCAP_ue \e'[0m'           # end underline
 set -gx LESS_TERMCAP_us \e'[04;38;5;146m' # begin underline
 
+
diff --git a/fish/functions/fish_prompt.fish b/fish/functions/fish_prompt.fish
index 04abe4b..96bfa3e 100755
--- a/fish/functions/fish_prompt.fish
+++ b/fish/functions/fish_prompt.fish
@@ -13,6 +13,9 @@ function _git_current_branch -d "Output git's current branch name"
   end ^/dev/null | sed -e 's|^refs/heads/||'
 end
 
+function fish_title --description 'Show current path (abbreviated) in iTerm tab title '
+   echo (prompt_pwd)
+end
 
 function fish_prompt --description 'Write out the prompt'
 
diff --git a/fish/functions/gr.fish b/fish/functions/gr.fish
index 4c2722d..1a0ee1b 100644
--- a/fish/functions/gr.fish
+++ b/fish/functions/gr.fish
@@ -1,3 +1,12 @@
 
 # git root
-alias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup || pwd`'
+function gr --description "Jump to the git root"
+	set -l repo_info (command git rev-parse --git-dir --is-inside-git-dir --is-bare-repository --is-inside-work-tree --short HEAD ^/dev/null)
+  	test -n "$repo_info"; or return
+
+	set -l cd_up_path (command git rev-parse --show-cdup)
+
+	if test -n $cd_up_path
+		cd $cd_up_path
+	end
+end
diff --git a/fish/functions/ls.fish b/fish/functions/ls.fish
index 33c3d8b..fd54db2 100644
--- a/fish/functions/ls.fish
+++ b/fish/functions/ls.fish
@@ -7,6 +7,8 @@
 if begin
     type gls 1>/dev/null 2>/dev/null
     or command ls --version 1>/dev/null 2>/dev/null
+
+    set -x CLICOLOR_FORCE 1
   end
   # This is GNU ls
   function ls --description "List contents of directory"
@@ -22,11 +24,11 @@ if begin
     set param $param --human-readable
     set param $param --sort=extension
     set param $param --group-directories-first
-	if isatty 1
+    if isatty 1
       set param $param --indicator-style=classify
     end
 
-    eval "env CLICOLOR_FORCE=1 command $ls $param $argv"
+    eval $ls $param "$argv"
   end
 
   if not set -q LS_COLORS
diff --git a/fish/path.fish b/fish/path.fish
index 6e28fad..aa9d014 100644
--- a/fish/path.fish
+++ b/fish/path.fish
@@ -13,8 +13,10 @@ for entry in (string split \n $PATH_DIRS)
     # resolve the {$HOME} substitutions
     set -l resolved_path (eval echo $entry)
     if test -d "$resolved_path"; # and not contains $resolved_path $PATH
-        set -x PA $PA "$resolved_path"
+        set PA $PA "$resolved_path"
     end
 end
 
-set -x PATH $PA
\ No newline at end of file
+set PA $PA /Users/paulirish/.rvm/gems/ruby-2.2.1/bin
+
+set --export PATH $PA
\ No newline at end of file
diff --git a/setup-a-new-machine.sh b/setup-a-new-machine.sh
index 7b5996c..7e60889 100755
--- a/setup-a-new-machine.sh
+++ b/setup-a-new-machine.sh
@@ -215,5 +215,7 @@ sh .osx
 # symlink it up!
 ./symlink-setup.sh
 
+# add manual symlink for .ssh/config and probably .config/fish
+
 ###
 ##############################################################################################################


================================================
FILE: test/fixtures/file-moves.diff
================================================
commit a12f64cfa2388b1d07659149db3a77314c9836c8
Author: Scott Baker <bakers@canbytel.com>
Date:   Thu Feb 25 08:31:54 2016 -0800

    Moves

diff --git a/appveyor.yml b/appveyor.yml
index a6fb95e..43d61c3 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-install:
+FOOinstall:
   - git clone --depth 1 https://github.com/sstephenson/bats.git
 
 build: false
diff --git a/circle.yml b/circle.yml
deleted file mode 100644
index 18a04a3..0000000
--- a/circle.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-machine:
-  environment:
-
-
-dependencies:
-  pre:
-    - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";
-    - sudo apt-get -y update
-    - sudo apt-get -y install shellcheck;
-
-  post:
-    - git clone --depth 1 https://github.com/sstephenson/bats.git
-
-checkout:
-  post:
-    - git submodule update --init
-
-test:
-  override:
-    - bats/bin/bats test/*.bats
-    - shellcheck diff-so-fancy update-deps.sh
diff --git a/hello.txt b/hello.txt
new file mode 100644
index 0000000..0c767bc
--- /dev/null
+++ b/hello.txt
@@ -0,0 +1 @@
+HI THERE
diff --git a/package.json b/package.json
deleted file mode 100644
index c7a1ab2..0000000
--- a/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-  "name": "diff-so-fancy",
-  "version": "0.5.0",
-  "description": "Good-lookin' diffs with diff-highlight and more",
-  "bin": {
-    "diff-so-fancy": "diff-so-fancy",
-    "diff-highlight": "third_party/diff-highlight/diff-highlight"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/so-fancy/diff-so-fancy.git"
-  },
-  "keywords": [
-    "git",
-    "diff",
-    "fancy",
-    "good-lookin'",
-    "diff-highlight",
-    "color",
-    "readable",
-    "highlight"
-  ],
-  "author": "Paul Irish",
-  "license": "MIT",
-  "bugs": {
-    "url": "https://github.com/so-fancy/diff-so-fancy/issues"
-  },
-  "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"
-}
diff --git a/square.yml b/square.yml
new file mode 100644
index 0000000..18a04a3
--- /dev/null
+++ b/square.yml
@@ -0,0 +1,21 @@
+machine:
+  environment:
+
+
+dependencies:
+  pre:
+    - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";
+    - sudo apt-get -y update
+    - sudo apt-get -y install shellcheck;
+
+  post:
+    - git clone --depth 1 https://github.com/sstephenson/bats.git
+
+checkout:
+  post:
+    - git submodule update --init
+
+test:
+  override:
+    - bats/bin/bats test/*.bats
+    - shellcheck diff-so-fancy update-deps.sh


================================================
FILE: test/fixtures/file-perms.diff
================================================
diff --git a/circle.yml b/circle.yml
old mode 100644
new mode 100755
diff --git a/foo.json b/foo.json
new file mode 100644
index 0000000..316a815
--- /dev/null
+++ b/foo.json
@@ -0,0 +1,29 @@
+{
+  "name": "diff-so-fancy",
+  "version": "0.5.1",
+  "description": "Good-lookin' diffs with diff-highlight and more",
+  "bin": {
+    "diff-so-fancy": "diff-so-fancy",
+    "diff-highlight": "third_party/diff-highlight/diff-highlight"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/so-fancy/diff-so-fancy.git"
+  },
+  "keywords": [
+    "git",
+    "diff",
+    "fancy",
+    "good-lookin'",
+    "diff-highlight",
+    "color",
+    "readable",
+    "highlight"
+  ],
+  "author": "Paul Irish",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/so-fancy/diff-so-fancy/issues"
+  },
+  "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"
+}
diff --git a/package.json b/package.json
deleted file mode 100644
index 316a815..0000000
--- a/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-  "name": "diff-so-fancy",
-  "version": "0.5.1",
-  "description": "Good-lookin' diffs with diff-highlight and more",
-  "bin": {
-    "diff-so-fancy": "diff-so-fancy",
-    "diff-highlight": "third_party/diff-highlight/diff-highlight"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/so-fancy/diff-so-fancy.git"
-  },
-  "keywords": [
-    "git",
-    "diff",
-    "fancy",
-    "good-lookin'",
-    "diff-highlight",
-    "color",
-    "readable",
-    "highlight"
-  ],
-  "author": "Paul Irish",
-  "license": "MIT",
-  "bugs": {
-    "url": "https://github.com/so-fancy/diff-so-fancy/issues"
-  },
-  "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"
-}


================================================
FILE: test/fixtures/file-rename.diff
================================================
diff --git a/Changes.new b/bin/Changes.new
similarity index 100%
rename from Changes.new
rename to bin/Changes.new
diff --git a/dist.ini b/dist.ini
index ca8c2ad..d2874a4 100644
--- a/dist.ini
+++ b/dist.ini
@@ -1,4 +1,4 @@
-name    = csvgrep
+name    = csvgrepper
 author  = Neil Bowers <neilb@cpan.org>
 license = Perl_5
 copyright_holder = Neil Bowers
@@ -9,6 +9,7 @@ version = 0.05
 [@Basic]
 [PkgVersion]
 [AutoPrereqs]
+[BAZ]
 
 [GithubMeta]
 [MetaJSON]


================================================
FILE: test/fixtures/file_copy.diff
================================================
diff --git a/first_file b/copied_file
similarity index 100%
copy from first_file
copy to copied_file


================================================
FILE: test/fixtures/file_with_space.diff
================================================
diff --git a/a b b/a b
new file mode 100644
index 0000000..e69de29


================================================
FILE: test/fixtures/first-three-line.diff
================================================
commit fbf440dd9c32a60f9bc97693a6bd7b5ca87ec9fc
Author: Steve Mao <maochenyan@gmail.com>
Date:   Wed Mar 9 19:21:27 2016 +1100

    0.6.2

diff --git package.json package.json
index 7379c98..3cba6ee 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
   "name": "diff-so-fancy",
-  "version": "0.6.1",
+  "version": "0.6.2",
   "description": "Good-lookin' diffs with diff-highlight and more",
   "bin": {
     "diff-so-fancy": "diff-so-fancy",


================================================
FILE: test/fixtures/hg.diff
================================================
diff -r 82e55d328c8c hello.c
--- a/hello.c	Fri Aug 26 01:21:28 2005 -0700
+++ b/hello.c	Fri Dec 29 14:37:26 2017 -0800
@@ -1,16 +1,15 @@
 /*
  * hello.c
  *
- * Placed in the public domain by Bryan O'Sullivan
- *
  * This program is not covered by patents in the United States or other
  * countries.
  */
 
-#include <stdio.h>
+#include <fstdio.h>
 
 int main(int argc, char **argv)
 {
 	printf("hello, world!\n");
+	exit 99;
 	return 0;
 }


================================================
FILE: test/fixtures/hunk_no_comma.diff
================================================
diff --git i/file w/file
index 90be1f3..294186e 100644
--- i/file
+++ w/file
@@ -1 +1 @@
-before
+after


================================================
FILE: test/fixtures/latin1.diff
================================================
diff --git a/test.txt b/test.txt
index 5836369..51ccf71 100644
--- a/test.txt
+++ b/test.txt
@@ -1,6 +1 @@
-é
-
+H cam h saw he conqurd


================================================
FILE: test/fixtures/leading-dashes.diff
================================================
diff --git i/lib/awful/widget/keyboardlayout.lua w/lib/awful/widget/keyboardlayout.lua
index f9142b8..b6e8ce0 100644
--- i/lib/awful/widget/keyboardlayout.lua
+++ w/lib/awful/widget/keyboardlayout.lua
@@ -113,7 +113,7 @@ keyboardlayout.xkeyboard_country_code = {
     ["za"] = true,    -- South Africa
 }

--- Callback for updaing current layout
+-- Callback for updating current layout.
 local function update_status (self)
     self._current = awesome.xkb_get_layout_group();
     local text = ""


================================================
FILE: test/fixtures/ls-function.diff
================================================
diff --git a/fish/functions/ls.fish b/fish/functions/ls.fish
index 33c3d8b..fd54db2 100644
--- a/fish/functions/ls.fish
+++ b/fish/functions/ls.fish
@@ -7,6 +7,8 @@
 if begin
     type gls 1>/dev/null 2>/dev/null
     or command ls --version 1>/dev/null 2>/dev/null
+
+    set -x CLICOLOR_FORCE 1
   end
   # This is GNU ls
   function ls --description "List contents of directory"
@@ -22,11 +24,11 @@ if begin
     set param $param --human-readable
     set param $param --sort=extension
     set param $param --group-directories-first
-	if isatty 1
+    if isatty 1
       set param $param --indicator-style=classify
     end
 
-    eval "env CLICOLOR_FORCE=1 command $ls $param $argv"
+    eval $ls $param "$argv"
   end
-
   if not set -q LS_COLORS

================================================
FILE: test/fixtures/mnemonicprefix.diff
================================================
diff --git i/diff-so-fancy w/diff-so-fancy
index 2323d9b..a280985 100755
--- i/diff-so-fancy
+++ w/diff-so-fancy
@@ -48,10 +48,6 @@ colorize_context_line () {
   $SED -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g"
 }
 
-mark_empty_lines () {
-  $SED -E "s/^$color_code_regex[\+\-]$reset_color\s*$/$invert_color\1 $reset_escape/g"
-}
-
 strip_leading_symbols () {
   # strip the + and -
   $SED -E "s/^($color_code_regex)[\+\-]/\1 /g"
@@ -83,7 +79,6 @@ cat $input \
   | $diff_highlight \
   | format_diff_header \
   | colorize_context_line \
-  | mark_empty_lines \
   | strip_leading_symbols \
   | strip_first_column \
   | print_header_clean
diff --git i/libs/header_clean/header_clean.pl w/libs/header_clean/header_clean.pl
index 23df5e7..54b3da1 100755
--- i/libs/header_clean/header_clean.pl
+++ w/libs/header_clean/header_clean.pl
@@ -9,6 +9,8 @@ my $remove_file_delete_header = 1;
 my $clean_permission_changes  = 1;
 my $change_hunk_indicators    = 1;
 
+use Data::Dump::Color;
+
 #################################################################################
 
 my $ansi_sequence_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,3})[mK])?/;
@@ -29,12 +31,12 @@ for (my $i = 0; $i <= $#input; $i++) {
 	########################################
 	# Find the first file: --- a/README.md #
 	########################################
-	} elsif ($line =~ /^$ansi_sequence_regex--- (a\/)?(.+?)(\e|$)/) {
+	} elsif ($line =~ /^$ansi_sequence_regex--- ([abiwco]\/)?(.+?)(\e|$)/) {
 		$file_1 = $5;
 
 		# Find the second file on the next line: +++ b/README.md
 		my $next = $input[++$i];
-		$next    =~ /^$ansi_sequence_regex\+\+\+ (b\/)?(.+?)(\e|$)/;
+		$next    =~ /^$ansi_sequence_regex\+\+\+ ([abiwco]\/)?(.+?)(\e|$)/;
 		if ($1) {
 			print $1; # Print out whatever color we're using
 		}
diff --git i/test/header_clean.bats w/test/header_clean.bats
index 4a3e7ee..a8385a5 100644
--- i/test/header_clean.bats
+++ w/test/header_clean.bats
@@ -50,3 +50,8 @@ output=$( load_fixture "file-moves" | $diff_so_fancy )
 	assert_output --partial '@ setup-a-new-machine.sh:33 @'
 	assert_output --partial '@ setup-a-new-machine.sh:218 @'
 }
+
+@test "mnemonicprefix" {
+	output=$( load_fixture "mnemonicprefix" | $diff_so_fancy )
+	assert_output --partial '@ setup-a-new-machine.sh:33 @'
+}
diff --git c/hello.txt i/hello.txt
deleted file mode 100644
index 0c767bc..0000000
--- c/hello.txt
+++ /dev/null
@@ -1 +0,0 @@
-HI THERE


================================================
FILE: test/fixtures/move_with_content_change.diff
================================================
diff --git a/a.txt b/b.txt
similarity index 84%
rename from a.txt
rename to b.txt
index 6674929..1c877ab 100644
--- a/a.txt
+++ b/b.txt
@@ -2,4 +2,4 @@ height: '10px',
 width: '100%',
 display: 'block',
 position: 'absolute',
-bottom: '0',
+bottom: '0'


================================================
FILE: test/fixtures/noprefix.diff
================================================
diff --git setup-a-new-machine.sh setup-a-new-machine.sh
index 7b5996c..67eec2a 100755
--- setup-a-new-machine.sh
+++ setup-a-new-machine.sh
@@ -30,6 +30,7 @@ cp -R ~/.gnupg ~/migration/home
 cp /Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist ~/migration  # wifi
 
 cp ~/Library/Preferences/net.limechat.LimeChat.plist ~/migration
+cp ~/Library/Preferences/com.tinyspeck.slackmacgap.plist ~/migration
 
 cp -R ~/Library/Services ~/migration # automator stuff
 
@@ -215,5 +216,7 @@ sh .osx
 # symlink it up!
 ./symlink-setup.sh
 
+# add manual symlink for .ssh/config and probably .config/fish
+
 ###
 ##############################################################################################################


================================================
FILE: test/fixtures/recursive_default_as_mercurial.diff
================================================
diff -r -bu core/app.py language/app.py
--- core/app.py	2022-06-08 13:42:10.658920131 +0900
+++ language/app.py	2022-06-08 12:07:22.773069512 +0900
@@ -1,13 +1,13 @@
-from flask import Flask, abort
-from . import CORE_MODULES
+from flask import Flask
+from . import SECTION
 
 app = Flask(__name__)
 
-@app.route('/<name>')
+@app.route(f'/{SECTION}/<name>')
 def index(name):
-    if name in CORE_MODULES:
-        return "Welcome to the documentation for the core module"
-    abort(404)
+    return f"Welcome to the documentation for {name}"
 
+if __name__ == "__main__":
+    app.run(host="0.0.0.0",port=3000)
 
 
diff -r -bu core/__init__.py language/__init__.py
--- core/__init__.py	2022-06-08 12:16:35.203331282 +0900
+++ language/__init__.py	2022-06-08 12:03:32.464264288 +0900
@@ -1 +1 @@
-CORE_MODULES=['base','utils','status']
+SECTION="language"
diff -r -bu core/README.md language/README.md
--- core/README.md	2022-06-08 13:52:56.962174912 +0900
+++ language/README.md	2022-06-08 13:52:39.498090643 +0900
@@ -1,4 +1,4 @@
-# Core
+# Language
 
 ## Installation
 


================================================
FILE: test/fixtures/recursive_longhand_as_mercurial.diff
================================================
diff --recursive -u core/app.py language/app.py
--- core/app.py	2022-06-08 13:42:10.658920131 +0900
+++ language/app.py	2022-06-08 12:07:22.773069512 +0900
@@ -1,13 +1,13 @@
-from flask import Flask, abort
-from . import CORE_MODULES
+from flask import Flask
+from . import SECTION
 
 app = Flask(__name__)
 
-@app.route('/<name>')
+@app.route(f'/{SECTION}/<name>')
 def index(name):
-    if name in CORE_MODULES:
-        return "Welcome to the documentation for the core module"
-    abort(404)
+    return f"Welcome to the documentation for {name}"
 
+if __name__ == "__main__":
+    app.run(host="0.0.0.0",port=3000)
 
 
diff --recursive -u core/__init__.py language/__init__.py
--- core/__init__.py	2022-06-08 12:16:35.203331282 +0900
+++ language/__init__.py	2022-06-08 12:03:32.464264288 +0900
@@ -1 +1 @@
-CORE_MODULES=['base','utils','status']
+SECTION="language"
diff --recursive -u core/README.md language/README.md
--- core/README.md	2022-06-08 13:52:56.962174912 +0900
+++ language/README.md	2022-06-08 13:52:39.498090643 +0900
@@ -1,4 +1,4 @@
-# Core
+# Language
 
 ## Installation
 


================================================
FILE: test/fixtures/remove_empty_file.diff
================================================
commit 0148734e753690a9207ebcb8cd117f343e230dec
Author: Scott Baker <fake@domain.com>
Date:   Wed Jul 19 08:12:45 2017 -0700

    remvo

diff --git a/empty_file.txt b/empty_file.txt
deleted file mode 100644
index e69de29..0000000


================================================
FILE: test/fixtures/remove_slashn_eof.diff
================================================
diff --git a/test.txt b/test.txt
index 4cb29ea..54d55bf 100644
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
 one
 two
-three
+three
\ No newline at end of file


================================================
FILE: test/fixtures/single-line-remove.diff
================================================
diff --git test/data/readywaitasset.js test/data/readywaitasset.js
deleted file mode 100644
index 2308965..0000000
--- test/data/readywaitasset.js
+++ /dev/null
@@ -1 +0,0 @@
-var delayedMessage = "It worked";


================================================
FILE: test/fixtures/truecolor.diff
================================================
diff --git package.json package.json
index 97965ab..f3ce90a 100644
--- package.json
+++ package.json
@@ -13,8 +13,8 @@
     "url": "git+https://github.com/so-fancy/diff-so-fancy.git"
   },
   "keywords": [
-    "git",
     "diff",
+    "git",
     "fancy",
     "good-lookin'",
     "diff-highlight",


================================================
FILE: test/fixtures/unicode.diff
================================================
diff --git a/unicodes b/unicodes
index 223f57d..1c2066d 100644
--- a/unicodes
+++ b/unicodes
@@ -1 +1 @@
-aao
+åäöç


================================================
FILE: test/fixtures/weird.diff
================================================
diff -r 62e478a3f1c8 -r 47aeb87ce9cd doc/manual.xml.head
--- a/doc/manual.xml.head
+++ b/doc/manual.xml.head
@@ -8352,7 +8352,7 @@
 <row><entry>-a</entry><entry>attach a file to a message</entry></row>
 <row><entry>-b</entry><entry>specify a blind carbon-copy (BCC) address</entry></row>
 <row><entry>-c</entry><entry>specify a carbon-copy (Cc) address</entry></row>
-<row><entry>-d</entry><entry>log debugging output to ~/.muttdebug0 if mutt was complied with +DEBUG; it can range from 1-5 and affects verbosity (a value of 2 is recommended)</entry></row>
+<row><entry>-d</entry><entry>log debugging output to ~/.muttdebug0 if mutt was compiled with +DEBUG; it can range from 1-5 and affects verbosity (a value of 2 is recommended)</entry></row>
 <row><entry>-D</entry><entry>print the value of all Mutt variables to stdout</entry></row>
 <row><entry>-E</entry><entry>edit the draft (-H) or include (-i) file</entry></row>
 <row><entry>-e</entry><entry>specify a config command to be run after initialization files are read</entry></row>


================================================
FILE: test/git-config.bats
================================================
#!/usr/bin/env bats

# Used by both `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
	load 'test_helper/bats-support/load'
	load 'test_helper/bats-assert/load'
	load 'test_helper/util'
}

# ansi related values
escape=$'\e'
ansi_bold=1
ansi_dim=2
ansi_ul=4
ansi_reverse=7

# hash of colors
declare -Ag ansi_colors=(
    [black]='0'
    [red]='1'
    [green]='2'
    [yellow]='3'
    [blue]='4'
    [magenta]='5'
    [cyan]='6'
    [white]='7'
    [default]='9'
)

# get a foreground or background color code
ansi_color() {
    color=$1
    incr=$2
    base_code=$((30+$incr))

    # handle bright prefix
    if [[ "${1}" =~ (bright)(.*) ]]
    then
        color=${BASH_REMATCH[2]}
        base_code=$((90+$incr))
    fi
    code=$(($base_code+${ansi_colors[$color]}))
    echo "$code"
}

# get a foreground color code
fg_color() {
    ansi_color $1 0
}

# get a background color code
bg_color() {
    ansi_color $1 10
}

# get rgb color codes from hex
rgb_color() {
    incr=$2
    base_code=$((38+$incr))
    [[ $1 =~ ^.(..)(..)(..)$ ]]
    rgb1="${BASH_REMATCH[1]}"
    rgb2="${BASH_REMATCH[2]}"
    rgb3="${BASH_REMATCH[3]}"
    echo "${base_code};2;$(( 16#${rgb1} ));$(( 16#${rgb2} ));$(( 16#${rgb3} ))"
}

# get a foreground color code
fg_rgb_color() {
    rgb_color $1 0
}

# get a background color code
bg_rgb_color() {
    rgb_color $1 10
}

# build config using passed in values
setup_dsf_git_config() {
  GIT_CONFIG="$(dsf_test_git_config)" || return $?
  cat > "${GIT_CONFIG}" <<EOF || return $?
[color "diff"]
$1

EOF
  export GIT_CONFIG
  export GIT_CONFIG_NOSYSTEM=1
}

setup_file() {
	__load_imports__
	# setup_default_dsf_git_config
}

setup() {
	__load_imports__
}

teardown_file() {
	teardown_default_dsf_git_config
}

# General description of how colors are applied
# meta = header
# frag = @ filenames
# func = function name after frag
# old  = - lines
# new  = + lines

@test "Test color attributes are removed" {
    config="
	meta = bold ul blink italic strike green no-reverse
	frag = nobold nodim noul noblink noreverse noitalic nostrike blue white
	func = dim reverse white purple
	old = no-bold no-dim no-ul no-blink no-reverse no-italic no-strike red yellow
	new = brightgreen
	whitespace = red reverse
    "
    setup_dsf_git_config "$config"

	output=$( load_fixture "leading-dashes" | $diff_so_fancy )
	run printf "%s" "$output"

    fg_green=$(fg_color green)
    fg_blue=$(fg_color blue)
    fg_red=$(fg_color red)
    fg_brightgreen=$(fg_color brightgreen)
    bg_white=$(bg_color white)
    bg_yellow=$(bg_color yellow)

    assert_line --index 0 --partial  "${escape}[${ansi_bold};${ansi_ul};${fg_green}m"
    assert_line --index 3 --partial  "${escape}[${fg_blue};${bg_white}m@"
    assert_line --index 6 --partial  "${escape}[${fg_red};${bg_yellow}m--"
    assert_line --index 7 --partial  "${escape}[${fg_brightgreen}m--"
}

@test "Test normal and default git colors" {
    config="
	frag = normal default
	old = default
    "
    setup_dsf_git_config "$config"

	output=$( load_fixture "leading-dashes" | $diff_so_fancy )
	run printf "%s" "$output"

    fg_default=$(fg_color default)
    bg_default=$(bg_color default)

    assert_line --index 3 --partial  "${escape}[${bg_default}m@"
    assert_line --index 6 --partial  "${escape}[${fg_default}m--"
}

# Special characters in git config must be quoted (word or all values)
@test "Test rgb git colors" {
    rgb="#408050"
    config="
	frag = \"${rgb}\" default
	old = normal \"${rgb}\"
    "
    setup_dsf_git_config "$config"

	output=$( load_fixture "leading-dashes" | $diff_so_fancy )
	run printf "%s" "$output"

    fg_rgb=$(fg_rgb_color "${rgb}")
    bg_default=$(bg_color default)
    bg_rgb=$(bg_rgb_color "${rgb}")

    assert_line --index 3 --partial  "${escape}[${fg_rgb};${bg_default}m@"
    assert_line --index 6 --partial  "${escape}[${bg_rgb}m--"
}


================================================
FILE: test/git_ansi_color.pl
================================================
#!/usr/bin/env perl

use strict;
use warnings;
use v5.16;

###############################################################################
###############################################################################

compare_color("\e[1;91m"               , git_ansi_color("brightred bold")    , "brightred bold");
compare_color("\e[1;31m"               , git_ansi_color("red bold")          , "red bold");
compare_color("\e[1;32m"               , git_ansi_color("green bold")        , "green bold");
compare_color("\e[33m"                 , git_ansi_color("yellow")            , "yellow");
compare_color("\e[1;7;32m"             , git_ansi_color("green bold reverse"), "green bold reverse");
compare_color("\e[1;35m"               , git_ansi_color("magenta bold")      , "magenta bold");
compare_color("\e[1;38;5;146m"         , git_ansi_color("146 bold")          , "146 bold");
compare_color("\e[1;38;5;146;48;5;22m" , git_ansi_color("146 bold 22")       , "146 bold 22");
compare_color("\e[1;34;40m"            , git_ansi_color("blue black bold")   , "blue black gold");
compare_color("\e[93m"                 , git_ansi_color("11")                , "11");
compare_color("\e[7;31m"               , git_ansi_color("red reverse")       , "red reverse");
compare_color("\e[1;31;48;5;52m"       , git_ansi_color("red bold 52")       , "red bold 52");
compare_color("\e[92;48;5;20m"         , git_ansi_color("10 20")             , "10 20");
compare_color("\e[30;47m"              , git_ansi_color("0 7")               , "0 7");
compare_color("\e[94;105m"             , git_ansi_color("12 13")             , "12 13");
compare_color("\e[1;38;5;254;48;5;255m", git_ansi_color("254 bold 255")      , "254 bold 255");
compare_color("\e[1;38;5;238;42m"      , git_ansi_color("238 bold green")    , "238 bold green");
compare_color("\e[1;38;5;238;42m"      , git_ansi_color("238 green bold")    , "238 green bold");

###############################################################################
###############################################################################

sub compare_color {
	my ($one, $two, $desc) = @_;

	if ($one ne $two) {
		#k($one, $two);
		#printf("No match for %-20s %s / %s\n", $desc, ansi_to_human($one), ansi_to_human($two));
		printf("%-20s %sFAIL%s  %s ne %s\n", $desc, color('red'), color(), ansi_to_human($one), ansi_to_human($two));
	} else {
		printf("%-20s %sOK%s\n", $desc, color('green'), color());
	}
}

# Convert an ANSI sequence to something printable
sub ansi_to_human {
	my $str = shift();

	$str =~ s/\e\[/[/g;
	$str =~ s/m\b/]/g;

	return $str;
}

# Alternate (unused) method to parse git config colors
sub git_ansi_color2 {
	my $str   = shift();
	my @parts = split(' ', $str);

	if (!@parts) {
		return '';
	}

	my %map = qw(
		black 30 red 31 green 32 yellow 33 blue 34 magenta 35 cyan 36 white 37
		bold 1 reverse 7
	);

	k(\@parts);

	my @ret;
	my $first = 1;
	foreach my $item (@parts) {
		my $val = $map{$item};

		if (!$val) {
			if (!$first && $item > 10) {
				$val = $item + 10;
			}
			push(@ret, (38,5,$val));
		} else {
			# Background color
			if (!$first && $val > 10) {
				$val += 10;
				$first = 0;
			}
			push(@ret, $val);
		}

		if ($val > 10) {
			$first = 0;
		}
	}

	my $ret = "\e[" . join(";", @ret) . "m";

	print ansi_to_human($ret);

	return $ret;
}

# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
sub git_ansi_color {
	my $str   = shift();
	my @parts = split(' ', $str);

	if (!@parts) {
		return '';
	}
	my $colors = {
		'black'   => 0,
		'red'     => 1,
		'green'   => 2,
		'yellow'  => 3,
		'blue'    => 4,
		'magenta' => 5,
		'cyan'    => 6,
		'white'   => 7,
	};

	my @ansi_part = ();

	if (grep { /bold/ } @parts) {
		push(@ansi_part, "1");
	}

	if (grep { /reverse/ } @parts) {
		push(@ansi_part, "7");
	}

	# Remove parts that aren't colors
	@parts = grep { exists $colors->{$_} || is_numeric($_) } @parts;

	my $fg  = $parts[0] // "";
	my $bg  = $parts[1] // "";

	#############################################

	# It's an numeric value, so it's an 8 bit color
	if (is_numeric($fg)) {
		if ($fg < 8) {
			push(@ansi_part, $fg + 30);
		} elsif ($fg < 16) {
			push(@ansi_part, $fg + 82);
		} else {
			push(@ansi_part, "38;5;$fg");
		}
	# It's a simple 16 color OG ansi
	} elsif ($fg) {
		my $bright    = $fg =~ s/bright//;
		my $color_num = $colors->{$fg} + 30;

		if ($bright) { $color_num += 60; } # Set bold

		push(@ansi_part, $color_num);
	}

	#############################################

	# It's an numeric value, so it's an 8 bit color
	if (is_numeric($bg)) {
		if ($bg < 8) {
			push(@ansi_part, $bg + 40);
		} elsif ($bg < 16) {
			push(@ansi_part, $bg + 92);
		} else {
			push(@ansi_part, "48;5;$bg");
		}
	# It's a simple 16 color OG ansi
	} elsif ($bg) {
		my $bright    = $bg =~ s/bright//;
		my $color_num = $colors->{$bg} + 40;

		if ($bright) { $color_num += 60; } # Set bold

		push(@ansi_part, $color_num);
	}

	#############################################

	my $ansi_str = join(";", @ansi_part);
	my $ret      = "\e[" . $ansi_str . "m";

	return $ret;
}

sub is_numeric {
	my $s = shift();

	if ($s =~ /^\d+$/) {
		return 1;
	}

	return 0;
}

sub trim {
	my $s = shift();
	if (!defined($s) || length($s) == 0) { return ""; }
	$s =~ s/^\s*//;
	$s =~ s/\s*$//;

	return $s;
}

# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
	my $str = shift();

	# If we're NOT connected to a an interactive terminal don't do color
	#if (-t STDOUT == 0) { return ''; }

	# No string sent in, so we just reset
	if (!length($str) || $str eq 'reset') { return "\e[0m"; }

	# Some predefined colors
	my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
	$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;

	# Get foreground/background and any commands
	my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
	my ($bc)      = $str =~ /on_(\d{1,3})$/g;

	# Some predefined commands
	my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
	my $cmd_num = $cmd_map{$cmd // 0};

	my $ret = '';
	if ($cmd_num)     { $ret .= "\e[${cmd_num}m"; }
	if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
	if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }

	return $ret;
}

# Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
	if (eval { require Data::Dump::Color }) {
		*k = sub { Data::Dump::Color::dd(@_) };
	} else {
		require Data::Dumper;
		*k = sub { print Data::Dumper::Dumper(\@_) };
	}

	sub kd {
		k(@_);

		printf("Died at %2\$s line #%3\$s\n",caller());
		exit(15);
	}
}

# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4


================================================
FILE: test/test_helper/util.bash
================================================

diff_so_fancy="$BATS_TEST_DIRNAME/../diff-so-fancy"

load_fixture() {
  local name="$1"
  cat "$BATS_TEST_DIRNAME/fixtures/${name}.diff"
}

set_env() {
  export LC_CTYPE="en_US.UTF-8"
}

dsf_test_git_config() {
  printf '%s/gitconfig' "${BATS_TMPDIR}"
}

# applying colors so ANSI color values will match
# FIXME: not everyone will have these set, so we need to test for that.
setup_default_dsf_git_config() {
  GIT_CONFIG="$(dsf_test_git_config)" || return $?
  cat > "${GIT_CONFIG}" <<EOF || return $?
[color "diff"]
	meta = 227
	frag = magenta bold
	commit = 227 bold
	old = red bold
	new = green bold
	whitespace = red reverse
	func = brightyellow

[color "diff-highlight"]
	oldNormal = red bold
	oldHighlight = red bold 52
	newNormal = green bold
	newHighlight = green bold 22
EOF
  export GIT_CONFIG
  export GIT_CONFIG_NOSYSTEM=1
}

teardown_default_dsf_git_config() {
  local test_config
  test_config="$(dsf_test_git_config)" || return $?
  [ ! -f "${test_config}" ] || rm -f "${test_config}"
  GIT_CONFIG=/dev/null
}


================================================
FILE: third_party/ansi-reveal/ansi-reveal.pl
================================================
#!/usr/bin/env perl

use strict;
use warnings;
use Getopt::Long;

my $raw = 0;
GetOptions(
	'raw' => \$raw,
);

if ($raw) {
	output_raw();
} else {
	output_human();
}

###############################################

sub output_human {
	my $reset = "\e[0m";

	while (my $l = <>) {
		$l =~ s/(\e\[.*?m)/dump_ansi($1)/eg;

		## Basic reset
		#$l =~ s/(\e\[0?m)/${reset}[RESET]/g;

		## Bold text
		#$l =~ s/(\e\[1m)/${reset}[BOLD]/g;

		## Inverted text
		#$l =~ s/(\e\[7m)/${reset}. "[INVRT]$1"/eg;
		#$l =~ s/(\e\[7;(\d+)m)/${reset}. "[INVRT" . sprintf("%03d",($2-30)) . "]$1"/eg;

		## Basic 16 color ANSI
		##$l =~ s/(\e\[(3[0-7])m)/${reset}. "[BASIC" . sprintf("%03d",($2-30)) . "]$1"/eg;
		#$l =~ s/(\e\[(3[0-7])m)/dump_ansi($1)/eg;
		##$l =~ s/(\e\[1;(3[0-7])m)/${reset}. "[BRIGH" . sprintf("%03d",($2-30)) . "]$1"/eg;
		#$l =~ s/(\e\[1;(3[0-7])m)/dump_ansi($1)/eg;

		## 256 Color/Background
		#$l =~ s/(\e\[38;0?5;(\d+)m)/${reset}. "[COLOR" . sprintf("%03d",$2) . "]$1"/eg;
		#$l =~ s/(\e\[48;0?5;(\d+)m)/${reset}. "[BACKG" . sprintf("%03d",$2) . "]$1"/eg;

		#$l =~ s/(\e\[1;(3[0-7]);48;5;(\d+)m)/${reset}. "[FGBG" . sprintf("%02d:%02d",($2-30),($3)) . "]$1"/eg;
		#$l =~ s/(\e\[1;38;5;(.*?)m)/${reset} . dump_ansi($1) . "$1"/eg;

		## 24bit Color/Background
		#$l =~ s/(\e\[38;2;(\d+);(\d+);(\d+)m)/${reset} . sprintf("[RGB#%02X%02X%02X",$2,$3,$4) . "]$1"/eg;
		#$l =~ s/(\e\[48;2;(\d+);(\d+);(\d+)m)/${reset} . sprintf("[RGBBG#%02X%02X%02X",$2,$3,$4) . "]$1"/eg;

		print $l;
	}
}

###############################################

sub output_raw {
	while (<>) {
		s/\e/\\e/g;
		print;
	}
}

sub dump_ansi {
	my $str   = shift();
	if ($str !~ /^\e/) {
		return "";
	}

	my $raw   = $str;
	my $human = $str =~ s/\e/ESC/rg;

	# Remove the ANSI control chars, we just want the payload
	$str =~ s/^\e\[//g;
	$str =~ s/m$//g;

	# Make the [HUMAN] text reset and white to make it easier to see
	my $ret  = "\e[0m";
	$ret    .= "\e[38;5;15m";

	my @parts = split(";",$str);

	#k(\@parts);

	my @basic_mapping = qw(BLACK RED GREEN YELLW BLUE MAGNT CYAN WHITE);

	if (!@parts) {
		$ret .= "[RESET]";
	}

	for (my $count = 0; $count < @parts; $count++) {
		my $p = $parts[$count];

		#print "[$count = '$p']\n";
		if ($p eq "1") {
			$ret .= "[BOLD]";
		} elsif ($p eq "0" || $p eq "") {
			$ret .= "[RESET]";
		} elsif ($p eq "7") {
			$ret .= "[REVERSE]";
		} elsif ($p eq "27") {
			$ret .= "[NOTREV]";
		} elsif ($p eq "38") {
			my $next  = $parts[$count + 1];
			my $color = $parts[$count + 2];

			$count += 2;

			$ret .= sprintf("[COLOR%03d]",$color);
		} elsif ($p eq "48") {
			my $next  = $parts[++$count];
			my $color = $parts[++$count];

			$count += 2;

			$ret .= sprintf("[BACKG%03d]",$color);
		} elsif ($p >= 30 and $p <= 37) {
			my $color = $p - 30;
			$color = $basic_mapping[$color];
			$ret .= "[$color]";
			#$ret .= sprintf("[BASIC%03d]",$color);
		} else {
			$ret .= "[UKN: $p]";
		}
	}

	# Append the ANSI color string to end of the human readable one
	$ret .= $raw;

	return $ret;
}

BEGIN {
	if (eval { require Data::Dump::Color }) {
		*k = sub { Data::Dump::Color::dd($_[0]) };
	} else {
		require Data::Dumper;
		*k = sub { print Data::Dumper::Dumper($_[0]) };
	}
}

# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4


================================================
FILE: third_party/build_fatpack/build.pl
================================================
#!/usr/bin/env perl

###########################################################################
# This will build a stand-a-lone version of diff-so-fancy using Fatpack
#
# Scott Baker - 2017-06-28
#
# Usage: perl build.pl [--output /tmp/diff-so-fancy]
###########################################################################

use strict;
use warnings;
use File::Basename;
use Cwd qw(abs_path getcwd);

my $args = argv();
my $ok   = has_fatpack();

if (!$ok) {
	printf("%sError:%s App::FatPacker must be installed to build diff-so-fancy\n",color('red_bold'),color('reset'));
	exit;
}

my $output_file = "/tmp/diff-so-fancy";
my $input_file  = "diff-so-fancy";

# Allow overriding the output file
if ($args->{output}) {
	$output_file = $args->{output};
}

my $dir  = dirname($0);
my $root = abs_path("$dir/../../");
my $cwd  = getcwd();

# Change to the root of the tree
chdir($root);
my $dsf_version = get_version($input_file);

my $cmd = "fatpack pack $input_file 2>/dev/null > $output_file";

my $exit = system($cmd);
$exit  >>= 8;

rmdir("fatlib"); # fatpack leaves empty fatlib dirs so we remove them
my $size = -s $output_file;

my $good  = color("82bold");
my $bad   = color("160bold");
my $warn  = color("226bold");
my $vers  = color("230bold");
my $white = color("15bold");
my $reset = color();

if (!$exit) {
	print "${good}Success:${reset} Wrote diff-so-fancy ${vers}v$dsf_version${reset} to $output_file ($size bytes)\n";
	chmod 0755,$output_file; # Make the output executable
} else {
	print "${bad}Error  :${reset} Fatpack failed to build $output_file with exit code: ${warn}$exit${reset}\n";
	print "${white}Command: $cmd$reset\n";
}

chdir($cwd);

#############################################################################

sub argv {
	my $ret = {};

	for (my $i = 0; $i < scalar(@ARGV); $i++) {
		# If the item starts with "-" it's a key
		if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_]\w*)/) && ($ARGV[$i] !~ /^-\w\w/)) {
			# If the next item does not start with "--" it's the value for this item
			if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
				$ret->{$key} = $ARGV[$i + 1];
			# Bareword like --verbose with no options
			} else {
				$ret->{$key}++;
			}
		}
	}

	# We're looking for a certain item
	if ($_[0]) { return $ret->{$_[0]}; }

	return $ret;
}

sub pfile {
	my $file = shift();
	if (!-r $file) { return ''; } # Make sure the file is readable

	my $ret = '';

	open(INPUT, "<", $file);
	while (<INPUT>) {
		$ret .= $_;
	}
	close INPUT;

	if (wantarray) {
		return split(/\n/,$ret);
	} else {
		return $ret;
	}
}

sub get_version {
	my $file  = shift();
	my @lines = pfile($file);

	foreach my $l (@lines) {
		if ($l =~ /\$VERSION\s+=\s+"(.+?)"/) {
			my $ver = $1;
			return $ver;
		}
	}

	return undef;
}

sub has_fatpack {
	my $out = `which fatpack 2>/dev/null`;

	return trim($out);
}

sub trim {
	my $s = shift();
	if (length($s) == 0) { return ""; }
	$s =~ s/^\s*|\s*$//g;

	return $s;
}

# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173'
sub color {
	my $str = shift();

	# If we're NOT connected to a an interactive terminal don't do color
	if (-t STDOUT == 0) { return ''; }

	# No string sent in, so we just reset
	if (!length($str) || $str eq 'reset') { return "\e[0m"; }

	# Some predefined colors
	my %color_map = qw(red 160 blue 21 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
	$str =~ s/$_/$color_map{$_}/g for keys %color_map;

	# Get foreground/background and any commands
	my ($fc,$cmd) = $str =~ /^(\d+)?_?(\w+)?/g;
	my ($bc)      = $str =~ /on_?(\d+)$/g;

	# Some predefined commands
	my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
	my $cmd_num = $cmd_map{$cmd || 0};

	my $ret = '';
	if ($cmd_num)     { $ret .= "\e[${cmd_num}m"; }
	if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
	if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }

	return $ret;
}

# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4


================================================
FILE: third_party/cli_bench/cli_bench.pl
================================================
#!/usr/bin/env perl

use strict;
use warnings;
use v5.16;

###############################################################################
# Used to benchmark optimizations to CLI scripts
#
# Usage: cli_bench [--num 50] 'cat /tmp/diff.patch | diff-so-fancy'
###############################################################################

use Time::HiRes qw(time);
use Getopt::Long;

###############################################################################
###############################################################################

my $num     = 50;
my $ignore  = 1;
my $details = 1;

my $ok = GetOptions(
	'num=i'    => \$num,
	'ignore=i' => \$ignore,
	'details!' => \$details,
);


my $cmd = trim(join(" ", @ARGV));

if (!$cmd) {
	die(usage());
}

$| = 0; # Disable output buffering

my @res;
my $out;
my $exit = 0;
for (my $i = 0; $i < ($num + $ignore); $i++) {
	my $start = time();
	$out      = `$cmd`;
	$exit     = $? >> 8;

	my $total = int((time() - $start) * 1000);
	push(@res, $total);

	print ".";
}

print "\n";

# Throw away the first X to give things time to warm up and be cached
@res = splice(@res, $ignore);

# Remove the top and bottom 10%
my $outlier = $num / 10;
@res = sort(@res);
@res = splice(@res, $outlier, $num - $outlier * 2);

my $avg = sprintf("%.1f", average(@res));

if ($details) {
	show_details(@res);
	print "\n";
}

print "Ran '$cmd' $num times with average completion time of $avg ms\n";

if ($exit != 0) {
	print $out;
}

###############################################################################
###############################################################################

sub show_details {
	my @res = @_;

	my $x   = {};
	my $max = 0;

	# Build a hash of all the times:count
	foreach my $time (@res) {
		$x->{$time}++;

		if ($x->{$time} > $max) {
			$max = $x->{$time};
		}
	}

	my $target_width = 100; # How wide we want the bar + text
	my $total        = scalar(@res);
	my $scale        = ($target_width - 15) / $max;

	print "\n";

	# Print out a basic histogram of the times
	foreach my $time (sort(keys %$x)) {
		my $count   = $x->{$time};
		my $percent = sprintf("%0.1f", ($count / $total) * 100);

		my $bar = "%" x ($count * $scale);
		print "$time ms: $bar ($percent%)\n";
	}
}

sub average {
	my $ret = 0;

	foreach (@_) {
		$ret += $_;
	}

	my $count = scalar(@_);
	$ret     /= $count;

	return $ret;
}

sub random_int {
	my $ret = rand() * 90 + 10;
	$ret    = int($ret);

	return $ret;
}

sub round {
	my $num = shift();

	#https://stackoverflow.com/questions/178539/how-do-you-round-a-floating-point-number-in-perl
	#my $ret = int($num + $num/abs($num * 2 || 1));

	my $ret;
	if ($num < 0) {
		$ret = int($num - 0.5);
	} else {
		$ret = int($num + 0.5);
	}

	return $ret;
}

sub trim {
	my $s = shift();
	if (!defined($s) || length($s) == 0) { return ""; }
	$s =~ s/^\s*//;
	$s =~ s/\s*$//;

	return $s;
}

# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
	my $str = shift();

	# If we're NOT connected to a an interactive terminal don't do color
	if (-t STDOUT == 0) { return ''; }

	# No string sent in, so we just reset
	if (!length($str) || $str eq 'reset') { return "\e[0m"; }

	# Some predefined colors
	my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
	$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;

	# Get foreground/background and any commands
	my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
	my ($bc)      = $str =~ /on_(\d{1,3})$/g;

	# Some predefined commands
	my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
	my $cmd_num = $cmd_map{$cmd // 0};

	my $ret = '';
	if ($cmd_num)     { $ret .= "\e[${cmd_num}m"; }
	if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
	if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }

	return $ret;
}

sub file_get_contents {
	my $file = shift();
	open (my $fh, "<", $file) or return undef;

	my $ret;
	while (<$fh>) { $ret .= $_; }

	return $ret;
}

sub file_put_contents {
	my ($file, $data) = @_;
	open (my $fh, ">", $file) or return undef;

	print $fh $data;
	return length($data);
}

# Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
	if (eval { require Data::Dump::Color }) {
		*k = sub { Data::Dump::Color::dd(@_) };
	} else {
		require Data::Dumper;
		*k = sub { print Data::Dumper::Dumper(\@_) };
	}

	sub kd {
		k(@_);

		printf("Died at %2\$s line #%3\$s\n",caller());
		exit(15);
	}
}

sub usage {
	return "Usage: $0 [--num 50] 'cat /tmp/simple.diff | diff-so-fancy'\n";
}

# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4


================================================
FILE: third_party/term-colors/term-colors.pl
================================================
#!/usr/bin/perl

use strict;
use warnings;

my $args   = join(" ",@ARGV);
my ($perl) = $args =~ /--perl/;
my ($both) = $args =~ /--both/;

# If we want both, we set perl also
if ($both) {
	$perl = 1;
}

# Term::ANSIColor didn't get 256 color constants until 4.0
if ($perl && has_term_ansicolor(4.0)) {
	require Term::ANSIColor;
	Term::ANSIColor->import(':constants','color','uncolor');

	#print "TERM::ANSIColor constant names:\n";
	term_ansicolor();
} else {
	my $padding = 2;
	my $section  = 1;
	my $grouping = 8;

	for (my $i = 0; $i < 256; $i++) {
		print set_bcolor($i); # Set the background color

		if (needs_white($i)) {
			print set_fcolor(15); # White
		} else {
			print set_fcolor(0); # Black
		}
		printf(" " x $padding . "%03d" . " " x $padding,$i);

		print set_fcolor(); # Reset both colors
		print "  ";         # Seperator

		if ($i == 15 || $i == 231) {
			print set_bcolor(); # Reset
			print "\n\n";
			$section  = 0;
			$grouping = 6;
			$padding = 4;
		} elsif ($section > 0 && ($section % $grouping == 0)) {
			print set_bcolor(); # Reset
			print "\n";
		}

		$section++;
	}
}

END {
	print set_fcolor(); # Reset the colors
	print "\n";
}

#################################################################################

sub has_term_ansicolor {
	my $version = shift();

	eval {
		# Check if we have Term::ANSIColor version 4.0
		require Term::ANSIColor;
		Term::ANSIColor->VERSION($version);
	};

	if ($@) {
		return 0;
	} else {
		return 1;
	}
}

sub set_fcolor {
	my $c = shift();

	my $ret = '';
	if (!defined($c)) { $ret = "\e[0m"; } # Reset the color
	else { $ret = "\e[38;5;${c}m"; }

	return $ret;
}

sub set_bcolor {
	my $c = shift();

	my $ret = '';
	if (!defined($c)) { $ret = "\e[0m"; } # Reset the color
	else { $ret .= "\e[48;5;${c}m"; }

	return $ret;
}

sub get_color_mapping {
	my $map = {};

	for (my $i = 0; $i < 256; $i++) {
		my $str = "\e[38;5;${i}m";
		my ($acc) = uncolor($str);

		$map->{$acc} = int($i);
	}

	return $map;
}

sub term_ansicolor {
	my @colors = get_color_names();
	my $map    = get_color_mapping();

	my $absolute = 0;
	my $group    = 0;
	my $grouping = 8;

	print "Showing Term::ANSIColor constant names\n\n";

	foreach my $name (@colors) {
		my $bg          = "on_$name";
		my $map_num     = int($map->{$name});
		my $perl_name   = sprintf("%6s",$name);
		my $ansi_number = sprintf("#%03i",$map_num);

		my $name_string = "";
		if ($both) {
			$name_string = "$perl_name / $ansi_number";
		} else {
			$name_string = "$perl_name";
		}

		if (needs_white($map_num)) {
			print color($bg) . "   " . color('bright_white') . $name_string . "   ";
		} else {
			print color($bg) . "   " . color("black") . $name_string . "   ";
		}
		print color('reset') . "  ";

		$absolute++;
		$group++;

		if ($absolute == 16 || $absolute == 232) {
			print "\n\n";
			$group    = 0;
			$grouping = 6;
		} elsif ($group % $grouping == 0) {
			print "\n";
		}
	}
}

sub get_color_names {
	my @colors    = ();
	my ($r,$g,$b) = 0;

	for (my $i = 0; $i < 16; $i++) {
		my $name = "ansi$i";
		push(@colors,$name);
	}

	for ($r = 0; $r <= 5; $r++) {
		for ($g = 0; $g <= 5; $g++) {
			for ($b = 0; $b <= 5; $b++) {
				my $name = "rgb$r$g$b";
				push(@colors,$name);
			}
		}
	}

	for (my $i = 0; $i < 24; $i++) {
		my $name = "grey$i";
		push(@colors,$name);
	}

	return @colors;
}

sub needs_white {
	# Sorta lame, but it's a hard coded list of which background colors need a white foreground
	my @needs_white = qw(0 1 4 5 8 232 233 234 235 236 237 238 239 240 241 242 243 16 17 18
	19 20 21 22 28 52 53 54 55 25 56 57 58 59 60 88 89 90 91 92 93 124 125 29 30 31 26
	27 61 62 64 160 196 161 126 63 94 95 100 101 127 128 129 12 130 131 23 24);

	my $num = shift();
	my $ret = in_array($num, @needs_white);

	return $ret;
}

sub in_array {
	my ($needle, @haystack) = @_;

	foreach my $l (@haystack) {
		if ($l == $needle) { return 1; }
	}

	return 0;
}


================================================
FILE: update-deps.sh
================================================
#!/bin/bash

# initialize the bats components
git submodule sync && git submodule update --init
Download .txt
gitextract_6y16t5y0/

├── .circleci/
│   └── config.yml
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── awesomebot.yml
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── diff-so-fancy
├── diff-so-fancy.plugin.zsh
├── docs/
│   └── diff-so-fancy.1
├── hacking-and-testing.md
├── history.md
├── lib/
│   └── DiffHighlight.pm
├── package.json
├── pro-tips.md
├── report-bug.sh
├── reporting-bugs.md
├── test/
│   ├── bugs.bats
│   ├── diff-so-fancy.bats
│   ├── fixtures/
│   │   ├── add_empty_file.diff
│   │   ├── add_remove_empty_lines.diff
│   │   ├── ansi_reset_no_number.diff
│   │   ├── binary-modified.diff
│   │   ├── chromium-modaltoelement.diff
│   │   ├── complex-hunks.diff
│   │   ├── diff_recursive.diff
│   │   ├── dotfiles.diff
│   │   ├── file-moves.diff
│   │   ├── file-perms.diff
│   │   ├── file-rename.diff
│   │   ├── file_copy.diff
│   │   ├── file_with_space.diff
│   │   ├── first-three-line.diff
│   │   ├── hg.diff
│   │   ├── hunk_no_comma.diff
│   │   ├── latin1.diff
│   │   ├── leading-dashes.diff
│   │   ├── ls-function.diff
│   │   ├── mnemonicprefix.diff
│   │   ├── move_with_content_change.diff
│   │   ├── noprefix.diff
│   │   ├── recursive_default_as_mercurial.diff
│   │   ├── recursive_longhand_as_mercurial.diff
│   │   ├── remove_empty_file.diff
│   │   ├── remove_slashn_eof.diff
│   │   ├── single-line-remove.diff
│   │   ├── truecolor.diff
│   │   ├── unicode.diff
│   │   └── weird.diff
│   ├── git-config.bats
│   ├── git_ansi_color.pl
│   └── test_helper/
│       └── util.bash
├── third_party/
│   ├── ansi-reveal/
│   │   └── ansi-reveal.pl
│   ├── build_fatpack/
│   │   └── build.pl
│   ├── cli_bench/
│   │   └── cli_bench.pl
│   └── term-colors/
│       └── term-colors.pl
└── update-deps.sh
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (173K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 424,
    "preview": "version: 2\njobs:\n  build:\n    machine: true\n    steps:\n      - checkout\n      - run:\n          name:\n          command: "
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 179,
    "preview": "---\n# Use `allow` to specify which dependencies to maintain\n\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\""
  },
  {
    "path": ".github/workflows/awesomebot.yml",
    "chars": 1326,
    "preview": "---\nname: Check links in README.md\n\non:\n  # Run daily\n  schedule: [{cron: \"0 1 * * *\"}]\n  push:\n    branches: [\"*\"]\n  pu"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 952,
    "preview": "name: Publish to npm\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n "
  },
  {
    "path": ".gitignore",
    "chars": 7,
    "preview": "/dist/\n"
  },
  {
    "path": ".gitmodules",
    "chars": 359,
    "preview": "[submodule \"test/bats\"]\n\tpath = test/bats\n\turl = https://github.com/bats-core/bats-core.git\n[submodule \"test/test_helper"
  },
  {
    "path": ".npmignore",
    "chars": 7,
    "preview": "/test/\n"
  },
  {
    "path": ".travis.yml",
    "chars": 1031,
    "preview": "os: linux\nlanguage: perl\nperl:\n  - blead\n  - dev\n  - 5.30\n  - 5.28\n  - 5.26\n  - 5.24\n  - 5.22\n  - 5.20\n  - 5.18\n  - 5.14"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 So Fancy team\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 4121,
    "preview": "# 🎩 diff-so-fancy  [![Circle CI build](https://circleci.com/gh/so-fancy/diff-so-fancy.svg?style=shield)](https://circlec"
  },
  {
    "path": "appveyor.yml",
    "chars": 178,
    "preview": "build: false\n\nbefore_test:\n  - git submodule sync\n  - git submodule update --init\n\ntest_script:\n  - C:\\cygwin\\bin\\bash -"
  },
  {
    "path": "diff-so-fancy",
    "chars": 38394,
    "preview": "#!/usr/bin/env perl\n\nmy $VERSION = \"1.4.7\";\n\n###########################################################################"
  },
  {
    "path": "diff-so-fancy.plugin.zsh",
    "chars": 394,
    "preview": "# Add our plugin diretory to user's path\n#\n# See following web page for explanation of the line \"ZERO=...\":\n# https://zd"
  },
  {
    "path": "docs/diff-so-fancy.1",
    "chars": 4045,
    "preview": ".\\\" -*- mode: troff; coding: utf-8 -*-\n.\\\" Automatically generated by Pod::Man 5.0102 (Pod::Simple 3.45)\n.\\\"\n.\\\" Standar"
  },
  {
    "path": "hacking-and-testing.md",
    "chars": 1627,
    "preview": "### Hacking\n\n```sh\n# fork and clone the diff-so-fancy repo.\ngit clone https://github.com/so-fancy/diff-so-fancy/ && cd d"
  },
  {
    "path": "history.md",
    "chars": 610,
    "preview": "## History\n\n`diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6"
  },
  {
    "path": "lib/DiffHighlight.pm",
    "chars": 6785,
    "preview": "package DiffHighlight;\n\nuse 5.008;\nuse warnings FATAL => 'all';\nuse strict;\n\n# Use the correct value for both UNIX and W"
  },
  {
    "path": "package.json",
    "chars": 966,
    "preview": "{\n  \"name\": \"diff-so-fancy\",\n  \"version\": \"1.4.6\",\n  \"description\": \"Good-lookin' diffs with diff-highlight and more\",\n "
  },
  {
    "path": "pro-tips.md",
    "chars": 2603,
    "preview": "# Pro-tips\n\n## One-off fanciness or a specific diff-so-fancy alias\n\nYou can do also do a one-off:\n\n```shell\ngit diff --c"
  },
  {
    "path": "report-bug.sh",
    "chars": 1297,
    "preview": "#!/bin/bash\n\nclipboard() {\n\tlocal copy_cmd\n\n\tif [ -n \"$PBCOPY_SERVER\" ]; then\n\t\tlocal body=\"\" # buffer\n\t\tbody=$(cat)\n\t\t#"
  },
  {
    "path": "reporting-bugs.md",
    "chars": 261,
    "preview": "### Reporting bugs\n\nIf you find a bug using the following command\n\n```sh\ngit diff HEAD..HEAD^\n```\n\nYou can use [report-b"
  },
  {
    "path": "test/bugs.bats",
    "chars": 3212,
    "preview": "#!/usr/bin/env bats\n\n# Used by both `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() {\n\tlo"
  },
  {
    "path": "test/diff-so-fancy.bats",
    "chars": 7873,
    "preview": "#!/usr/bin/env bats\n\n# Helper invoked by `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() "
  },
  {
    "path": "test/fixtures/add_empty_file.diff",
    "chars": 235,
    "preview": "commit ef0e63dbd7e8df6cde4ee5599ad65db7820888ef\nAuthor: Scott Baker <fake@domain.com>\nDate:   Wed Jul 19 08:00:11 2017 -"
  },
  {
    "path": "test/fixtures/add_remove_empty_lines.diff",
    "chars": 128,
    "preview": "--- one.txt\t2020-04-23 10:15:29.193452325 -0700\n+++ two.txt\t2020-04-23 10:15:37.634463619 -0700\n@@ -1,5 +1,5 @@\n 1\n+\n 2\n"
  },
  {
    "path": "test/fixtures/ansi_reset_no_number.diff",
    "chars": 834,
    "preview": "\u001b[93mdiff --git a/history.md b/history.md\u001b[m\n\u001b[93mindex f6776e0..a6b4546 100644\u001b[m\n\u001b[93m--- a/history.md\u001b[m\n\u001b[93m+++ b/h"
  },
  {
    "path": "test/fixtures/binary-modified.diff",
    "chars": 935,
    "preview": "\u001b[1;33mdiff --git a/cancel.png b/cancel.png\u001b[m\n\u001b[1;33mindex 667e10c..06fc49c 100644\u001b[m\nBinary files a/cancel.png and b/c"
  },
  {
    "path": "test/fixtures/chromium-modaltoelement.diff",
    "chars": 2109,
    "preview": "\u001b[1;33mdiff --git a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js b/third_party/WebKit/Source/devtools/front"
  },
  {
    "path": "test/fixtures/complex-hunks.diff",
    "chars": 1326,
    "preview": "\u001b[1;32mcommit 74804e377d4a54d1173d4393827d4e4b27e4d5d0\u001b[m\n\u001b[1;33mdiff --cc libs/header_clean/header_clean.pl\u001b[m\n\u001b[1;33mi"
  },
  {
    "path": "test/fixtures/diff_recursive.diff",
    "chars": 452,
    "preview": "Only in /var/tmp/b: donk\ndiff --recursive -u /var/tmp/a/foo/bar /var/tmp/b/foo/bar\n--- /var/tmp/a/foo/bar  2026-03-13 21"
  },
  {
    "path": "test/fixtures/dotfiles.diff",
    "chars": 18612,
    "preview": "\u001b[1;33mdiff --git a/.aliases b/.aliases\u001b[m\n\u001b[1;33mindex 30182ef..be9fb1d 100644\u001b[m\n\u001b[1;33m--- a/.aliases\u001b[m\n\u001b[1;33m+++ b"
  },
  {
    "path": "test/fixtures/file-moves.diff",
    "chars": 4081,
    "preview": "\u001b[33mcommit a12f64cfa2388b1d07659149db3a77314c9836c8\u001b[m\nAuthor: Scott Baker <bakers@canbytel.com>\nDate:   Thu Feb 25 08:"
  },
  {
    "path": "test/fixtures/file-perms.diff",
    "chars": 3053,
    "preview": "\u001b[38;5;227mdiff --git a/circle.yml b/circle.yml\u001b[m\n\u001b[38;5;227mold mode 100644\u001b[m\n\u001b[38;5;227mnew mode 100755\u001b[m\n\u001b[38;5;22"
  },
  {
    "path": "test/fixtures/file-rename.diff",
    "chars": 460,
    "preview": "diff --git a/Changes.new b/bin/Changes.new\nsimilarity index 100%\nrename from Changes.new\nrename to bin/Changes.new\ndiff "
  },
  {
    "path": "test/fixtures/file_copy.diff",
    "chars": 101,
    "preview": "diff --git a/first_file b/copied_file\nsimilarity index 100%\ncopy from first_file\ncopy to copied_file\n"
  },
  {
    "path": "test/fixtures/file_with_space.diff",
    "chars": 67,
    "preview": "diff --git a/a b b/a b\nnew file mode 100644\nindex 0000000..e69de29\n"
  },
  {
    "path": "test/fixtures/first-three-line.diff",
    "chars": 453,
    "preview": "commit fbf440dd9c32a60f9bc97693a6bd7b5ca87ec9fc\nAuthor: Steve Mao <maochenyan@gmail.com>\nDate:   Wed Mar 9 19:21:27 2016"
  },
  {
    "path": "test/fixtures/hg.diff",
    "chars": 442,
    "preview": "diff -r 82e55d328c8c hello.c\n--- a/hello.c\tFri Aug 26 01:21:28 2005 -0700\n+++ b/hello.c\tFri Dec 29 14:37:26 2017 -0800\n@"
  },
  {
    "path": "test/fixtures/hunk_no_comma.diff",
    "chars": 104,
    "preview": "diff --git i/file w/file\nindex 90be1f3..294186e 100644\n--- i/file\n+++ w/file\n@@ -1 +1 @@\n-before\n+after\n"
  },
  {
    "path": "test/fixtures/latin1.diff",
    "chars": 252,
    "preview": "\u001b[38;5;227mdiff --git a/test.txt b/test.txt\u001b[m\n\u001b[38;5;227mindex 5836369..51ccf71 100644\u001b[m\n\u001b[38;5;227m--- a/test.txt\u001b[m\n"
  },
  {
    "path": "test/fixtures/leading-dashes.diff",
    "chars": 499,
    "preview": "diff --git i/lib/awful/widget/keyboardlayout.lua w/lib/awful/widget/keyboardlayout.lua\nindex f9142b8..b6e8ce0 100644\n---"
  },
  {
    "path": "test/fixtures/ls-function.diff",
    "chars": 960,
    "preview": "\u001b[1;33mdiff --git a/fish/functions/ls.fish b/fish/functions/ls.fish\u001b[m\n\u001b[1;33mindex 33c3d8b..fd54db2 100644\u001b[m\n\u001b[1;33m--"
  },
  {
    "path": "test/fixtures/mnemonicprefix.diff",
    "chars": 3057,
    "preview": "\u001b[1;33mdiff --git i/diff-so-fancy w/diff-so-fancy\u001b[m\n\u001b[1;33mindex 2323d9b..a280985 100755\u001b[m\n\u001b[1;33m--- i/diff-so-fancy\u001b"
  },
  {
    "path": "test/fixtures/move_with_content_change.diff",
    "chars": 253,
    "preview": "diff --git a/a.txt b/b.txt\nsimilarity index 84%\nrename from a.txt\nrename to b.txt\nindex 6674929..1c877ab 100644\n--- a/a."
  },
  {
    "path": "test/fixtures/noprefix.diff",
    "chars": 896,
    "preview": "\u001b[1;33mdiff --git setup-a-new-machine.sh setup-a-new-machine.sh\u001b[m\n\u001b[1;33mindex 7b5996c..67eec2a 100755\u001b[m\n\u001b[1;33m--- se"
  },
  {
    "path": "test/fixtures/recursive_default_as_mercurial.diff",
    "chars": 1074,
    "preview": "diff -r -bu core/app.py language/app.py\n--- core/app.py\t2022-06-08 13:42:10.658920131 +0900\n+++ language/app.py\t2022-06-"
  },
  {
    "path": "test/fixtures/recursive_longhand_as_mercurial.diff",
    "chars": 1098,
    "preview": "diff --recursive -u core/app.py language/app.py\n--- core/app.py\t2022-06-08 13:42:10.658920131 +0900\n+++ language/app.py\t"
  },
  {
    "path": "test/fixtures/remove_empty_file.diff",
    "chars": 230,
    "preview": "commit 0148734e753690a9207ebcb8cd117f343e230dec\nAuthor: Scott Baker <fake@domain.com>\nDate:   Wed Jul 19 08:12:45 2017 -"
  },
  {
    "path": "test/fixtures/remove_slashn_eof.diff",
    "chars": 161,
    "preview": "diff --git a/test.txt b/test.txt\nindex 4cb29ea..54d55bf 100644\n--- a/test.txt\n+++ b/test.txt\n@@ -1,3 +1,3 @@\n one\n two\n-"
  },
  {
    "path": "test/fixtures/single-line-remove.diff",
    "chars": 210,
    "preview": "diff --git test/data/readywaitasset.js test/data/readywaitasset.js\ndeleted file mode 100644\nindex 2308965..0000000\n--- t"
  },
  {
    "path": "test/fixtures/truecolor.diff",
    "chars": 469,
    "preview": "\u001b[33mdiff --git package.json package.json\u001b[m\n\u001b[33mindex 97965ab..f3ce90a 100644\u001b[m\n\u001b[33m--- package.json\u001b[m\n\u001b[33m+++ pac"
  },
  {
    "path": "test/fixtures/unicode.diff",
    "chars": 203,
    "preview": "\u001b[1;33mdiff --git a/unicodes b/unicodes\u001b[m\r\n\u001b[1;33mindex 223f57d..1c2066d 100644\u001b[m\r\n\u001b[1;33m--- a/unicodes\u001b[m\r\n\u001b[1;33m++"
  },
  {
    "path": "test/fixtures/weird.diff",
    "chars": 1039,
    "preview": "diff -r 62e478a3f1c8 -r 47aeb87ce9cd doc/manual.xml.head\n--- a/doc/manual.xml.head\n+++ b/doc/manual.xml.head\n@@ -8352,7 "
  },
  {
    "path": "test/git-config.bats",
    "chars": 3888,
    "preview": "#!/usr/bin/env bats\n\n# Used by both `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() {\n\tlo"
  },
  {
    "path": "test/git_ansi_color.pl",
    "chars": 6748,
    "preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse v5.16;\n\n#############################################################"
  },
  {
    "path": "test/test_helper/util.bash",
    "chars": 1028,
    "preview": "\ndiff_so_fancy=\"$BATS_TEST_DIRNAME/../diff-so-fancy\"\n\nload_fixture() {\n  local name=\"$1\"\n  cat \"$BATS_TEST_DIRNAME/fixtu"
  },
  {
    "path": "third_party/ansi-reveal/ansi-reveal.pl",
    "chars": 3255,
    "preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse Getopt::Long;\n\nmy $raw = 0;\nGetOptions(\n\t'raw' => \\$raw,\n);\n\nif ($raw"
  },
  {
    "path": "third_party/build_fatpack/build.pl",
    "chars": 3927,
    "preview": "#!/usr/bin/env perl\n\n###########################################################################\n# This will build a sta"
  },
  {
    "path": "third_party/cli_bench/cli_bench.pl",
    "chars": 4683,
    "preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse v5.16;\n\n#############################################################"
  },
  {
    "path": "third_party/term-colors/term-colors.pl",
    "chars": 3901,
    "preview": "#!/usr/bin/perl\n\nuse strict;\nuse warnings;\n\nmy $args   = join(\" \",@ARGV);\nmy ($perl) = $args =~ /--perl/;\nmy ($both) = $"
  },
  {
    "path": "update-deps.sh",
    "chars": 96,
    "preview": "#!/bin/bash\n\n# initialize the bats components\ngit submodule sync && git submodule update --init\n"
  }
]

About this extraction

This page contains the full source code of the so-fancy/diff-so-fancy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (145.6 KB), approximately 54.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!