/ diff-so-fancy
diff-so-fancy
   1  #!/usr/bin/env perl
   2  
   3  # This chunk of stuff was generated by App::FatPacker. To find the original
   4  # file's code, look for the end of this BEGIN block or the string 'FATPACK'
   5  BEGIN {
   6  my %fatpacked;
   7  
   8  $fatpacked{"DiffHighlight.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'DIFFHIGHLIGHT';
   9    package DiffHighlight;
  10    
  11    use 5.008;
  12    use warnings FATAL => 'all';
  13    use strict;
  14    
  15    # Use the correct value for both UNIX and Windows (/dev/null vs nul)
  16    use File::Spec;
  17    
  18    my $NULL = File::Spec->devnull();
  19    
  20    # Highlight by reversing foreground and background. You could do
  21    # other things like bold or underline if you prefer.
  22    our @OLD_HIGHLIGHT = (
  23    	undef,
  24    	"\e[7m",
  25    	"\e[27m",
  26    );
  27    our @NEW_HIGHLIGHT = (
  28    	$OLD_HIGHLIGHT[0],
  29    	$OLD_HIGHLIGHT[1],
  30    	$OLD_HIGHLIGHT[2],
  31    );
  32    
  33    
  34    
  35    my $RESET = "\x1b[m";
  36    my $COLOR = qr/\x1b\[[0-9;]*m/;
  37    my $BORING = qr/$COLOR|\s/;
  38    
  39    my @removed;
  40    my @added;
  41    my $in_hunk;
  42    my $graph_indent = 0;
  43    
  44    our $line_cb = sub { print @_ };
  45    our $flush_cb = sub { local $| = 1 };
  46    
  47    # Count the visible width of a string, excluding any terminal color sequences.
  48    sub visible_width {
  49    	local $_ = shift;
  50    	my $ret = 0;
  51    	while (length) {
  52    		if (s/^$COLOR//) {
  53    			# skip colors
  54    		} elsif (s/^.//) {
  55    			$ret++;
  56    		}
  57    	}
  58    	return $ret;
  59    }
  60    
  61    # Return a substring of $str, omitting $len visible characters from the
  62    # beginning, where terminal color sequences do not count as visible.
  63    sub visible_substr {
  64    	my ($str, $len) = @_;
  65    	while ($len > 0) {
  66    		if ($str =~ s/^$COLOR//) {
  67    			next
  68    		}
  69    		$str =~ s/^.//;
  70    		$len--;
  71    	}
  72    	return $str;
  73    }
  74    
  75    sub handle_line {
  76    	my $orig = shift;
  77    	local $_ = $orig;
  78    
  79    	# match a graph line that begins a commit
  80    	if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space
  81    	         $COLOR?\*$COLOR?[ ]   # a "*" with its trailing space
  82    	      (?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|"
  83    	                         [ ]*  # trailing whitespace for merges
  84    	    /x) {
  85    	        my $graph_prefix = $&;
  86    
  87    		# We must flush before setting graph indent, since the
  88    		# new commit may be indented differently from what we
  89    		# queued.
  90    		flush();
  91    		$graph_indent = visible_width($graph_prefix);
  92    
  93    	} elsif ($graph_indent) {
  94    		if (length($_) < $graph_indent) {
  95    			$graph_indent = 0;
  96    		} else {
  97    			$_ = visible_substr($_, $graph_indent);
  98    		}
  99    	}
 100    
 101    	if (!$in_hunk) {
 102    		$line_cb->($orig);
 103    		$in_hunk = /^$COLOR*\@\@ /;
 104    	}
 105    	elsif (/^$COLOR*-/) {
 106    		push @removed, $orig;
 107    	}
 108    	elsif (/^$COLOR*\+/) {
 109    		push @added, $orig;
 110    	}
 111    	else {
 112    		flush();
 113    		$line_cb->($orig);
 114    		$in_hunk = /^$COLOR*[\@ ]/;
 115    	}
 116    
 117    	# Most of the time there is enough output to keep things streaming,
 118    	# but for something like "git log -Sfoo", you can get one early
 119    	# commit and then many seconds of nothing. We want to show
 120    	# that one commit as soon as possible.
 121    	#
 122    	# Since we can receive arbitrary input, there's no optimal
 123    	# place to flush. Flushing on a blank line is a heuristic that
 124    	# happens to match git-log output.
 125    	if (!length) {
 126    		$flush_cb->();
 127    	}
 128    }
 129    
 130    sub flush {
 131    	# Flush any queued hunk (this can happen when there is no trailing
 132    	# context in the final diff of the input).
 133    	show_hunk(\@removed, \@added);
 134    	@removed = ();
 135    	@added = ();
 136    }
 137    
 138    sub highlight_stdin {
 139    	while (<STDIN>) {
 140    		handle_line($_);
 141    	}
 142    	flush();
 143    }
 144    
 145    # Ideally we would feed the default as a human-readable color to
 146    # git-config as the fallback value. But diff-highlight does
 147    # not otherwise depend on git at all, and there are reports
 148    # of it being used in other settings. Let's handle our own
 149    # fallback, which means we will work even if git can't be run.
 150    sub color_config {
 151    	my ($key, $default) = @_;
 152    
 153    	# Removing the redirect speeds up execution by about 12ms
 154    	#my $s = `git config --get-color $key 2>$NULL`;
 155    	my $s = `git config --get-color $key`;
 156    
 157    	return length($s) ? $s : $default;
 158    }
 159    
 160    sub show_hunk {
 161    	my ($a, $b) = @_;
 162    
 163    	# If one side is empty, then there is nothing to compare or highlight.
 164    	if (!@$a || !@$b) {
 165    		$line_cb->(@$a, @$b);
 166    		return;
 167    	}
 168    
 169    	# If we have mismatched numbers of lines on each side, we could try to
 170    	# be clever and match up similar lines. But for now we are simple and
 171    	# stupid, and only handle multi-line hunks that remove and add the same
 172    	# number of lines.
 173    	if (@$a != @$b) {
 174    		$line_cb->(@$a, @$b);
 175    		return;
 176    	}
 177    
 178    	my @queue;
 179    	for (my $i = 0; $i < @$a; $i++) {
 180    		my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
 181    		$line_cb->($rm);
 182    		push @queue, $add;
 183    	}
 184    	$line_cb->(@queue);
 185    }
 186    
 187    sub highlight_pair {
 188    	my @a = split_line(shift);
 189    	my @b = split_line(shift);
 190    
 191    	# Find common prefix, taking care to skip any ansi
 192    	# color codes.
 193    	my $seen_plusminus;
 194    	my ($pa, $pb) = (0, 0);
 195    	while ($pa < @a && $pb < @b) {
 196    		if ($a[$pa] =~ /$COLOR/) {
 197    			$pa++;
 198    		}
 199    		elsif ($b[$pb] =~ /$COLOR/) {
 200    			$pb++;
 201    		}
 202    		elsif ($a[$pa] eq $b[$pb]) {
 203    			$pa++;
 204    			$pb++;
 205    		}
 206    		elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
 207    			$seen_plusminus = 1;
 208    			$pa++;
 209    			$pb++;
 210    		}
 211    		else {
 212    			last;
 213    		}
 214    	}
 215    
 216    	# Find common suffix, ignoring colors.
 217    	my ($sa, $sb) = ($#a, $#b);
 218    	while ($sa >= $pa && $sb >= $pb) {
 219    		if ($a[$sa] =~ /$COLOR/) {
 220    			$sa--;
 221    		}
 222    		elsif ($b[$sb] =~ /$COLOR/) {
 223    			$sb--;
 224    		}
 225    		elsif ($a[$sa] eq $b[$sb]) {
 226    			$sa--;
 227    			$sb--;
 228    		}
 229    		else {
 230    			last;
 231    		}
 232    	}
 233    
 234    	if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
 235    		return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT),
 236    		       highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT);
 237    	}
 238    	else {
 239    		return join('', @a),
 240    		       join('', @b);
 241    	}
 242    }
 243    
 244    # we split either by $COLOR or by character. This has the side effect of
 245    # leaving in graph cruft. It works because the graph cruft does not contain "-"
 246    # or "+"
 247    sub split_line {
 248    	local $_ = shift;
 249    	return utf8::decode($_) ?
 250    		map { utf8::encode($_); $_ }
 251    			map { /$COLOR/ ? $_ : (split //) }
 252    			split /($COLOR+)/ :
 253    		map { /$COLOR/ ? $_ : (split //) }
 254    		split /($COLOR+)/;
 255    }
 256    
 257    sub highlight_line {
 258    	my ($line, $prefix, $suffix, $theme) = @_;
 259    
 260    	my $start = join('', @{$line}[0..($prefix-1)]);
 261    	my $mid = join('', @{$line}[$prefix..$suffix]);
 262    	my $end = join('', @{$line}[($suffix+1)..$#$line]);
 263    
 264    	# If we have a "normal" color specified, then take over the whole line.
 265    	# Otherwise, we try to just manipulate the highlighted bits.
 266    	if (defined $theme->[0]) {
 267    		s/$COLOR//g for ($start, $mid, $end);
 268    		chomp $end;
 269    		return join('',
 270    			$theme->[0], $start, $RESET,
 271    			$theme->[1], $mid, $RESET,
 272    			$theme->[0], $end, $RESET,
 273    			"\n"
 274    		);
 275    	} else {
 276    		return join('',
 277    			$start,
 278    			$theme->[1], $mid, $theme->[2],
 279    			$end
 280    		);
 281    	}
 282    }
 283    
 284    # Pairs are interesting to highlight only if we are going to end up
 285    # highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
 286    # is just useless noise. We can detect this by finding either a matching prefix
 287    # or suffix (disregarding boring bits like whitespace and colorization).
 288    sub is_pair_interesting {
 289    	my ($a, $pa, $sa, $b, $pb, $sb) = @_;
 290    	my $prefix_a = join('', @$a[0..($pa-1)]);
 291    	my $prefix_b = join('', @$b[0..($pb-1)]);
 292    	my $suffix_a = join('', @$a[($sa+1)..$#$a]);
 293    	my $suffix_b = join('', @$b[($sb+1)..$#$b]);
 294    
 295    	return visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ ||
 296    	       visible_substr($prefix_b, $graph_indent) !~ /^$COLOR*\+$BORING*$/ ||
 297    	       $suffix_a !~ /^$BORING*$/ ||
 298    	       $suffix_b !~ /^$BORING*$/;
 299    }
 300  DIFFHIGHLIGHT
 301  
 302  s/^  //mg for values %fatpacked;
 303  
 304  my $class = 'FatPacked::'.(0+\%fatpacked);
 305  no strict 'refs';
 306  *{"${class}::files"} = sub { keys %{$_[0]} };
 307  
 308  if ($] < 5.008) {
 309    *{"${class}::INC"} = sub {
 310      if (my $fat = $_[0]{$_[1]}) {
 311        my $pos = 0;
 312        my $last = length $fat;
 313        return (sub {
 314          return 0 if $pos == $last;
 315          my $next = (1 + index $fat, "\n", $pos) || $last;
 316          $_ .= substr $fat, $pos, $next - $pos;
 317          $pos = $next;
 318          return 1;
 319        });
 320      }
 321    };
 322  }
 323  
 324  else {
 325    *{"${class}::INC"} = sub {
 326      if (my $fat = $_[0]{$_[1]}) {
 327        open my $fh, '<', \$fat
 328          or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
 329        return $fh;
 330      }
 331      return;
 332    };
 333  }
 334  
 335  unshift @INC, bless \%fatpacked, $class;
 336    } # END OF FATPACK CODE
 337  
 338  
 339  my $VERSION = "1.4.2";
 340  
 341  #################################################################################
 342  
 343  use v5.010;                                                 # Require Perl 5.10 for 'state' variables
 344  use warnings FATAL => 'all';
 345  use strict;
 346  
 347  use File::Spec;                                             # For catdir
 348  use File::Basename;                                         # For dirname
 349  use Cwd qw(abs_path);                                       # For realpath()
 350  use lib dirname(abs_path(File::Spec->catdir($0))) . "/lib"; # Add the local lib/ to @INC
 351  use DiffHighlight;
 352  
 353  my $remove_file_add_header     = 1;
 354  my $remove_file_delete_header  = 1;
 355  my $clean_permission_changes   = 1;
 356  my $patch_mode                 = 0;
 357  my $manually_color_lines       = 0; # Usually git/hg colorizes the lines, but for raw patches we use this
 358  my $change_hunk_indicators     = git_config_boolean("diff-so-fancy.changeHunkIndicators","true");
 359  my $strip_leading_indicators   = git_config_boolean("diff-so-fancy.stripLeadingSymbols","true");
 360  my $mark_empty_lines           = git_config_boolean("diff-so-fancy.markEmptyLines","true");
 361  my $use_unicode_dash_for_ruler = git_config_boolean("diff-so-fancy.useUnicodeRuler","true");
 362  my $ruler_width                = git_config("diff-so-fancy.rulerWidth", undef);
 363  my $git_strip_prefix           = git_config_boolean("diff.noprefix","false");
 364  my $has_stdin                  = has_stdin();
 365  
 366  my $ansi_color_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,10})[mK])?/;
 367  my $reset_color      = color("reset");
 368  my $bold             = color("bold");
 369  my $meta_color       = "";
 370  
 371  # Set the diff highlight colors from the config
 372  init_diff_highlight_colors();
 373  
 374  my ($file_1,$file_2);
 375  my $args              = argv(); # Hashref of all the ARGV stuff
 376  my $last_file_seen    = "";
 377  my $last_file_mode    = "";
 378  my $i                 = 0;
 379  my $in_hunk           = 0;
 380  my $columns_to_remove = 0;
 381  my $is_mercurial      = 0;
 382  my $color_forced      = 0; # Has the color been forced on/off
 383  
 384  # We try and be smart about whether we need to do line coloring, but
 385  # this is an option to force it on/off
 386  if ($args->{color_on}) {
 387  	$manually_color_lines = 1;
 388  	$color_forced         = 1;
 389  } elsif ($args->{color_off}) {
 390  	$manually_color_lines = 0;
 391  	$color_forced         = 1;
 392  }
 393  
 394  if ($args->{debug}) {
 395  	show_debug_info();
 396  	exit();
 397  }
 398  
 399  # `git add --patch` requires our output to match the number of lines from the
 400  # input. So, when patch mode is active, we print out empty lines to pad our
 401  # output to match any lines we've consumed.
 402  if ($args->{patch}) {
 403  	$patch_mode = 1;
 404  }
 405  
 406  # We only process ARGV if we don't have STDIN
 407  if (!$has_stdin) {
 408  	if ($args->{v} || $args->{version}) {
 409  		die(version());
 410  	} elsif ($args->{'set-defaults'}) {
 411  		my $ok = set_defaults();
 412  		exit;
 413  	} elsif ($args->{colors}) {
 414  		# We print this to STDOUT so we can redirect to bash to auto-set the colors
 415  		print get_default_colors();
 416  		exit;
 417  	} elsif (!%$args || $args->{help} || $args->{h}) {
 418  		my $first = check_first_run();
 419  
 420  		if (!$first) {
 421  			die(usage());
 422  		}
 423  	} else {
 424  		die("Missing input on STDIN\n");
 425  	}
 426  }
 427  
 428  #################################################################################
 429  #################################################################################
 430  
 431  # Check to see if were using default settings
 432  check_first_run();
 433  
 434  # The logic here is that we run all the lines through DiffHighlight first. This
 435  # highlights all the intra-word changes. Then we take those lines and send them
 436  # to do_dsf_stuff() to convert the diff to human readable d-s-f output and add
 437  # appropriate fanciness
 438  
 439  my @lines;
 440  local $DiffHighlight::line_cb = sub {
 441  	push(@lines,@_);
 442  
 443  	my $last_line = $lines[-1];
 444  
 445  	# Buffer X lines before we try and output anything
 446  	# Also make sure we're sending enough data to d-s-f to do it's magic.
 447  	# Certain things require a look-ahead line or two to function so
 448  	# we make sure we don't break on those sections prematurely
 449  	if (@lines > 24 && ($last_line !~ /^${ansi_color_regex}(---|index|old mode|similarity index|rename (from|to))/)) {
 450  		do_dsf_stuff(\@lines);
 451  		@lines = ();
 452  	}
 453  };
 454  
 455  my $line_count = 0;
 456  while (my $line = <STDIN>) {
 457  	# If the very first line of the diff doesn't start with ANSI color we're assuming
 458  	# it's a raw patch file, and we have to color the added/removed lines ourself
 459  	if (!$color_forced && $line_count == 0 && starts_with_ansi($line)) {
 460  		$manually_color_lines = 1;
 461  	}
 462  
 463  	my $ok = DiffHighlight::handle_line($line);
 464  	$line_count++;
 465  }
 466  
 467  # If we're mid hunk above process anything still pending
 468  DiffHighlight::flush();
 469  do_dsf_stuff(\@lines);
 470  
 471  #################################################################################
 472  #################################################################################
 473  
 474  sub do_dsf_stuff {
 475  	my $input = shift();
 476  
 477  	#print STDERR "START -------------------------------------------------\n";
 478  	#print STDERR join("",@$input);
 479  	#print STDERR "END ---------------------------------------------------\n";
 480  
 481  	while (my $line = shift(@$input)) {
 482  		######################################################
 483  		# Pre-process the line before we do any other markup #
 484  		######################################################
 485  
 486  		# If the first line of the input is a blank line, skip that
 487  		if ($i == 0 && $line =~ /^\s*$/) {
 488  			next;
 489  		}
 490  
 491  		######################
 492  		# End pre-processing #
 493  		######################
 494  
 495  		#######################################################################
 496  
 497  		####################################################################
 498  		# Look for git index and replace it horizontal line (header later) #
 499  		####################################################################
 500  		if ($line =~ /^${ansi_color_regex}index /) {
 501  			# Print the line color and then the actual line
 502  			$meta_color = $1 || get_config_color("meta");
 503  
 504  			# Get the next line without incrementing counter while loop
 505  			my $next = $input->[0] || "";
 506  			my ($file_1,$file_2);
 507  
 508  			# The line immediately after the "index" line should be the --- file line
 509  			# If it's not it's an empty file add/delete
 510  			if ($next !~ /^$ansi_color_regex(---|Binary files)/) {
 511  
 512  				# We fake out the file names since it's a raw add/delete
 513  				if ($last_file_mode eq "add") {
 514  					$file_1 = "/dev/null";
 515  					$file_2 = $last_file_seen;
 516  				} elsif ($last_file_mode eq "delete") {
 517  					$file_1 = $last_file_seen;
 518  					$file_2 = "/dev/null";
 519  				}
 520  			}
 521  
 522  			if ($file_1 && $file_2) {
 523  				print horizontal_rule($meta_color);
 524  				print $meta_color . file_change_string($file_1,$file_2) . "\n";
 525  				print horizontal_rule($meta_color);
 526  			}
 527  		#########################
 528  		# Look for the filename #
 529  		#########################
 530  		#                                            $4              $5
 531  		} elsif ($line =~ /^${ansi_color_regex}diff (-r|--git|--cc) (.*?)(\e| b\/|$)/) {
 532  
 533  			# Mercurial looks like: diff -r 82e55d328c8c hello.c
 534  			if ($4 eq "-r") {
 535  				$is_mercurial = 1;
 536  				$meta_color = get_config_color("meta");
 537  			# Git looks like: diff --git a/diff-so-fancy b/diff-so-fancy
 538  			} else {
 539  				$last_file_seen = $5;
 540  			}
 541  
 542  			$last_file_seen =~ s|^\w/||; # Remove a/ (and handle diff.mnemonicPrefix).
 543  			$in_hunk = 0;
 544  			if ($patch_mode) {
 545  				# we are consuming one line, and the debt must be paid
 546  				print "\n";
 547  			}
 548  		########################################
 549  		# Find the first file: --- a/README.md #
 550  		########################################
 551  		} elsif (!$in_hunk && $line =~ /^$ansi_color_regex--- (\w\/)?(.+?)(\e|\t|$)/) {
 552  			$meta_color = get_config_color("meta");
 553  
 554  			if ($git_strip_prefix) {
 555  				my $file_dir = $4 || "";
 556  				$file_1 = $file_dir . $5;
 557  			} else {
 558  				$file_1 = $5;
 559  			}
 560  
 561  			# Find the second file on the next line: +++ b/README.md
 562  			my $next = shift(@$input);
 563  			$next    =~ /^$ansi_color_regex\+\+\+ (\w\/)?(.+?)(\e|\t|$)/;
 564  			if ($1) {
 565  				print $1; # Print out whatever color we're using
 566  			}
 567  			if ($git_strip_prefix) {
 568  				my $file_dir = $4 || "";
 569  				$file_2 = $file_dir . $5;
 570  			} else {
 571  				$file_2 = $5;
 572  			}
 573  
 574  			if ($file_2 ne "/dev/null") {
 575  				$last_file_seen = $file_2;
 576  			}
 577  
 578  			# Print out the top horizontal line of the header
 579  			print $reset_color;
 580  			print horizontal_rule($meta_color);
 581  
 582  			# Mercurial coloring is slightly different so we need to hard reset colors
 583  			if ($is_mercurial) {
 584  				print $reset_color;
 585  			}
 586  
 587  			print $meta_color;
 588  			print file_change_string($file_1,$file_2) . "\n";
 589  
 590  			# Print out the bottom horizontal line of the header
 591  			print horizontal_rule($meta_color);
 592  		########################################
 593  		# Check for "@@ -3,41 +3,63 @@" syntax #
 594  		########################################
 595  		} elsif (!$change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
 596  			$in_hunk = 1;
 597  
 598  			print $line;
 599  		} elsif ($change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
 600  			$in_hunk = 1;
 601  
 602  			my $hunk_header = $4;
 603  			my $remain      = bleach_text($5);
 604  
 605  			# The number of colums to remove (1 or 2) is based on how many commas in the hunk header
 606  			$columns_to_remove   = (char_count(",",$hunk_header)) - 1;
 607  			# On single line removes there is NO comma in the hunk so we force one
 608  			if ($columns_to_remove <= 0) {
 609  				$columns_to_remove = 1;
 610  			}
 611  
 612  			if ($1) {
 613  				print $1; # Print out whatever color we're using
 614  			}
 615  
 616  			my ($orig_offset, $orig_count, $new_offset, $new_count) = parse_hunk_header($hunk_header);
 617  			#$last_file_seen = basename($last_file_seen);
 618  
 619  			# Figure out the start line
 620  			my $start_line = start_line_calc($new_offset,$new_count);
 621  
 622  			# Last function has it's own color
 623  			my $last_function_color = "";
 624  			if ($remain) {
 625  				$last_function_color = get_config_color("last_function");
 626  			}
 627  
 628  			# Check to see if we have the color for the fragment from git
 629  			if ($5 =~ /\e\[\d/) {
 630  				#print "Has ANSI color for fragment\n";
 631  			} else {
 632  				# We don't have the ANSI sequence so we shell out to get it
 633  				#print "No ANSI color for fragment\n";
 634  				my $frag_color = get_config_color("fragment");
 635  				print $frag_color;
 636  			}
 637  
 638  			print "@ $last_file_seen:$start_line \@${bold}${last_function_color}${remain}${reset_color}\n";
 639  		###################################
 640  		# Remove any new file permissions #
 641  		###################################
 642  		} elsif ($remove_file_add_header && $line =~ /^${ansi_color_regex}.*new file mode/) {
 643  			# Don't print the line (i.e. remove it from the output);
 644  			$last_file_mode = "add";
 645  			if ($patch_mode) {
 646  				print "\n";
 647  			}
 648  		######################################
 649  		# Remove any delete file permissions #
 650  		######################################
 651  		} elsif ($remove_file_delete_header && $line =~ /^${ansi_color_regex}deleted file mode/) {
 652  			# Don't print the line (i.e. remove it from the output);
 653  			$last_file_mode = "delete";
 654  			if ($patch_mode) {
 655  				print "\n";
 656  			}
 657  		################################
 658  		# Look for binary file changes #
 659  		################################
 660  		} elsif ($line =~ /^Binary files (\w\/)?(.+?) and (\w\/)?(.+?) differ/) {
 661  			my $change = file_change_string($2,$4);
 662  			print horizontal_rule($meta_color);
 663  			print "$meta_color$change (binary)\n";
 664  			print horizontal_rule($meta_color);
 665  		#####################################################
 666  		# Check if we're changing the permissions of a file #
 667  		#####################################################
 668  		} elsif ($clean_permission_changes && $line =~ /^${ansi_color_regex}old mode (\d+)/) {
 669  			my ($old_mode) = $4;
 670  			my $next = shift(@$input);
 671  
 672  			if ($1) {
 673  				print $1; # Print out whatever color we're using
 674  			}
 675  
 676  			my ($new_mode) = $next =~ m/new mode (\d+)/;
 677  
 678  			if ($patch_mode) {
 679  				print "\n";
 680  			}
 681  			print "$last_file_seen changed file mode from $old_mode to $new_mode\n";
 682  
 683  		###############
 684  		# File rename #
 685  		###############
 686  		} elsif ($line =~ /^${ansi_color_regex}similarity index (\d+)%/) {
 687  			my $simil = $4;
 688  
 689  			# If it's a move with content change we ignore this and the next two lines
 690  			if ($simil != 100) {
 691  				shift(@$input);
 692  				shift(@$input);
 693  				next;
 694  			}
 695  
 696  			my $next    = shift(@$input);
 697  			my ($file1) = $next =~ /rename from (.+?)(\e|\t|$)/;
 698  
 699  			$next       = shift(@$input);
 700  			my ($file2) = $next =~ /rename to (.+?)(\e|\t|$)/;
 701  
 702  			if ($file1 && $file2) {
 703  				# We may not have extracted this yet, so we pull from the config if not
 704  				$meta_color = get_config_color("meta");
 705  
 706  				my $change = file_change_string($file1,$file2);
 707  
 708  				print horizontal_rule($meta_color);
 709  				print $meta_color . $change . "\n";
 710  				print horizontal_rule($meta_color);
 711  			}
 712  
 713  			$i += 3; # We've consumed three lines
 714  			next;
 715  		#####################################
 716  		# Just a regular line, print it out #
 717  		#####################################
 718  		} else {
 719  			# Mark empty line with a red/green box indicating addition/removal
 720  			if ($mark_empty_lines) {
 721  				$line = mark_empty_line($line);
 722  			}
 723  
 724  			# Remove the correct number of leading " " or "+" or "-"
 725  			if ($strip_leading_indicators) {
 726  				$line = strip_leading_indicators($line,$columns_to_remove);
 727  			}
 728  			print $line;
 729  		}
 730  
 731  		$i++;
 732  	}
 733  }
 734  
 735  ######################################################################################################
 736  # End regular code, begin functions
 737  ######################################################################################################
 738  
 739  # Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805
 740  sub parse_hunk_header {
 741  	my ($line) = @_;
 742  	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = $line =~ /^\@\@+(?: -(\d+)(?:,(\d+))?)+ \+(\d+)(?:,(\d+))? \@\@+/;
 743  	$o_cnt = 1 unless defined $o_cnt;
 744  	$n_cnt = 1 unless defined $n_cnt;
 745  	return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
 746  }
 747  
 748  # Mark the first char of an empty line
 749  sub mark_empty_line {
 750  	my $line = shift();
 751  
 752  	my $reset_color  = "\e\\[0?m";
 753  	my $reset_escape = "\e\[m";
 754  	my $invert_color = "\e\[7m";
 755  	my $add_color    = $DiffHighlight::NEW_HIGHLIGHT[1];
 756  	my $del_color    = $DiffHighlight::OLD_HIGHLIGHT[1];
 757  
 758  	# This captures lines that do not have any ANSI in them (raw vanilla diff)
 759  	if ($line eq "+\n") {
 760  		$line = $invert_color . $add_color . " " . color('reset') . "\n";
 761  	# This captures lines that do not have any ANSI in them (raw vanilla diff)
 762  	} elsif ($line eq "-\n") {
 763  		$line = $invert_color . $del_color . " " . color('reset') . "\n";
 764  	# This handles everything else
 765  	} else {
 766  		$line =~ s/^($ansi_color_regex)[+-]$reset_color\s*$/$invert_color$1 $reset_escape\n/;
 767  	}
 768  
 769  	return $line;
 770  }
 771  
 772  # String to boolean
 773  sub boolean {
 774  	my $str = shift();
 775  	$str    = trim($str);
 776  
 777  	if ($str eq "" || $str =~ /^(no|false|0)$/i) {
 778  		return 0;
 779  	} else {
 780  		return 1;
 781  	}
 782  }
 783  
 784  # Get the git config
 785  sub git_config_raw {
 786  	my $cmd = "git config --list";
 787  	my @out = `$cmd`;
 788  
 789  	return \@out;
 790  }
 791  
 792  # Memoize fetching a textual item from the git config
 793  sub git_config {
 794  	my $search_key    = lc($_[0] || "");
 795  	my $default_value = lc($_[1] || "");
 796  
 797  	state $raw = {};
 798  	if (%$raw && $search_key) {
 799  		return $raw->{$search_key} || $default_value;
 800  	}
 801  
 802  	if ($args->{debug}) {
 803  		print "Parsing git config\n";
 804  	}
 805  
 806  	my $out = git_config_raw();
 807  
 808  	foreach my $line (@$out) {
 809  		if ($line =~ /=/) {
 810  			my ($key,$value) = split("=",$line,2);
 811  			$value =~ s/\s+$//;
 812  			$raw->{$key} = $value;
 813  		}
 814  	}
 815  
 816  	# If we're given a search key return that, else return the hash
 817  	if ($search_key) {
 818  		return $raw->{$search_key} || $default_value;
 819  	} else {
 820  		return $raw;
 821  	}
 822  }
 823  
 824  # Fetch a boolean item from the git config
 825  sub git_config_boolean {
 826  	my $search_key    = lc($_[0] || "");
 827  	my $default_value = lc($_[1] || 0); # Default to false
 828  
 829  	# If we're in a unit test, use the default (don't read the users config)
 830  	if (in_unit_test()) {
 831  		return boolean($default_value);
 832  	}
 833  
 834  	my $result = git_config($search_key,$default_value);
 835  	my $ret    = boolean($result);
 836  
 837  	return $ret;
 838  }
 839  
 840  # Check if we're inside of BATS
 841  sub in_unit_test {
 842  	if ($ENV{BATS_CWD}) {
 843  		return 1;
 844  	} else {
 845  		return 0;
 846  	}
 847  }
 848  
 849  sub get_less_charset {
 850  	my @less_char_vars = ("LESSCHARSET", "LESSCHARDEF", "LC_ALL", "LC_CTYPE", "LANG");
 851  	foreach my $key (@less_char_vars) {
 852  		my $val = $ENV{$key};
 853  
 854  		if (defined $val) {
 855  			return ($key, $val);
 856  		}
 857  	}
 858  
 859  	return ();
 860  }
 861  
 862  sub should_print_unicode {
 863  	if (-t STDOUT) {
 864  		# Always print unicode chars if we're not piping stuff, e.g. to less(1)
 865  		return 1;
 866  	}
 867  
 868  	# Otherwise, assume we're piping to less(1)
 869  	my ($less_env_var, $less_charset) = get_less_charset();
 870  	if ($less_charset && $less_charset =~ /utf-?8/i) {
 871  		return 1;
 872  	}
 873  
 874  	return 0;
 875  }
 876  
 877  # Try and be smart about what line the diff hunk starts on
 878  sub start_line_calc {
 879  	my ($line_num,$diff_context) = @_;
 880  	my $ret;
 881  
 882  	if ($line_num == 0 && $diff_context == 0) {
 883  		return 1;
 884  	}
 885  
 886  	# Git defaults to three lines of context
 887  	my $default_context_lines = 3;
 888  	# Three lines on either side, and the line itself = 7
 889  	my $expected_context      = ($default_context_lines * 2 + 1);
 890  
 891  	# The first three lines
 892  	if ($line_num == 1 && $diff_context < $expected_context) {
 893  		$ret = $diff_context - $default_context_lines;
 894  	} else {
 895  		$ret = $line_num + $default_context_lines;
 896  	}
 897  
 898  	if ($ret < 1) {
 899  		$ret = 1;
 900  	}
 901  
 902  	return $ret;
 903  }
 904  
 905  # Remove + or - at the beginning of the lines
 906  sub strip_leading_indicators {
 907  	my $line              = shift(); # Array passed in by reference
 908  	my $columns_to_remove = shift(); # Don't remove any lines by default
 909  
 910  	if ($columns_to_remove == 0) {
 911  		return $line; # Nothing to do
 912  	}
 913  
 914  	$line =~ s/^(${ansi_color_regex})([ +-]){${columns_to_remove}}/$1/;
 915  
 916  	if ($manually_color_lines) {
 917  		if (defined($5) && $5 eq "+") {
 918  			my $add_line_color = get_config_color("add_line");
 919  			$line              = $add_line_color . insert_reset_at_line_end($line);
 920  		} elsif (defined($5) && $5 eq "-") {
 921  			my $remove_line_color = get_config_color("remove_line");
 922  			$line                 = $remove_line_color . insert_reset_at_line_end($line);
 923  		}
 924  	}
 925  
 926  	return $line;
 927  }
 928  
 929  # Insert the color reset code at end of line, but before any newlines
 930  sub insert_reset_at_line_end {
 931  	my $line = shift();
 932  	$line =~ s/^(.*)([\n\r]+)?$/${1}${reset_color}${2}/;
 933  	return $line;
 934  }
 935  
 936  # Count the number of a given char in a string
 937  # https://www.perturb.org/display/1010_Perl_Count_occurrences_of_substring.html
 938  sub char_count {
 939  	my ($needle, $haystack) = @_;
 940  
 941  	my $count = () = ($haystack =~ /$needle/g);
 942  
 943  	return $count;
 944  }
 945  
 946  # Remove all ANSI codes from a string
 947  sub bleach_text {
 948  	my $str = shift();
 949  	$str    =~ s/\e\[\d*(;\d+)*m//mg;
 950  
 951  	return $str;
 952  }
 953  
 954  # Remove all trailing and leading spaces
 955  sub trim {
 956  	my $s = shift();
 957  	if (!$s) { return ""; }
 958  
 959  	$s =~ s/^\s*//u;
 960  	$s =~ s/\s*$//u;
 961  
 962  	return $s;
 963  }
 964  
 965  # Print a line of em-dash or line-drawing chars the full width of the screen
 966  sub horizontal_rule {
 967  	my $color = $_[0] || "";
 968  	my $width = get_terminal_width();
 969  
 970  	# em-dash http://www.fileformat.info/info/unicode/char/2014/index.htm
 971  	#my $dash = "\x{2014}";
 972  	# BOX DRAWINGS LIGHT HORIZONTAL http://www.fileformat.info/info/unicode/char/2500/index.htm
 973  	my $dash;
 974  	if ($use_unicode_dash_for_ruler && should_print_unicode()) {
 975  		#$dash = Encode::encode('UTF-8', "\x{2500}");
 976  		$dash = "\xE2\x94\x80";
 977  	} else {
 978  		$dash = "-";
 979  	}
 980  
 981  	# Draw the line
 982  	my $ret = $color . ($dash x $width) . "$reset_color\n";
 983  
 984  	return $ret;
 985  }
 986  
 987  sub file_change_string {
 988  	my $file_1 = shift();
 989  	my $file_2 = shift();
 990  
 991  	# If they're the same it's a modify
 992  	if ($file_1 eq $file_2) {
 993  		return "modified: $file_1";
 994  	# If the first is /dev/null it's a new file
 995  	} elsif ($file_1 eq "/dev/null") {
 996  		my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1];
 997  		return "added: $add_color$file_2$reset_color";
 998  	# If the second is /dev/null it's a deletion
 999  	} elsif ($file_2 eq "/dev/null") {
1000  		my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1];
1001  		return "deleted: $del_color$file_1$reset_color";
1002  	# If the files aren't the same it's a rename
1003  	} elsif ($file_1 ne $file_2) {
1004  		my ($old, $new) = DiffHighlight::highlight_pair($file_1,$file_2,{only_diff => 1});
1005  		# highlight_pair already includes reset_color, but adds newline characters that need to be trimmed off
1006  		$old = trim($old);
1007  		$new = trim($new);
1008  		return "renamed: $old$meta_color to $new"
1009  	# Something we haven't thought of yet
1010  	} else {
1011  		return "$file_1 -> $file_2";
1012  	}
1013  }
1014  
1015  # Check to see if STDIN is connected to an interactive terminal
1016  sub has_stdin {
1017  	my $i   = -t STDIN;
1018  	my $ret = int(!$i);
1019  
1020  	return $ret;
1021  }
1022  
1023  # We use this instead of Getopt::Long because it's faster and we're not parsing any
1024  # crazy arguments
1025  # Borrowed from: https://www.perturb.org/display/1153_Perl_Quick_extract_variables_from_ARGV.html
1026  sub argv {
1027  	my $ret = {};
1028  
1029  	for (my $i = 0; $i < scalar(@ARGV); $i++) {
1030  
1031  		# If the item starts with "-" it's a key
1032  		if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_-]*\w)$/) && ($ARGV[$i] !~ /^-\w\w/)) {
1033  			# If the next item does not start with "--" it's the value for this item
1034  			if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
1035  				$ret->{$key} = $ARGV[$i + 1];
1036  			# Bareword like --verbose with no options
1037  			} else {
1038  				$ret->{$key}++;
1039  			}
1040  		}
1041  	}
1042  
1043  	# We're looking for a certain item
1044  	if ($_[0]) { return $ret->{$_[0]}; }
1045  
1046  	return $ret;
1047  }
1048  
1049  # Output the command line usage for d-s-f
1050  sub usage {
1051  	my $out = color("white_bold") . version() . color("reset") . "\n";
1052  
1053  	$out .= "Usage:
1054  
1055  git diff --color | diff-so-fancy         # Use d-s-f on one diff
1056  cat diff.txt | diff-so-fancy             # Use d-s-f on a diff/patch file
1057  diff -u one.txt two.txt | diff-so-fancy  # Use d-s-f on unified diff output
1058  
1059  diff-so-fancy --colors                   # View the commands to set the recommended colors
1060  diff-so-fancy --set-defaults             # Configure git-diff to use diff-so-fancy and suggested colors
1061  diff-so-fancy --patch                    # Use diff-so-fancy in patch mode (interoperable with `git add --patch`)
1062  
1063  # Configure git to use d-s-f for *all* diff operations
1064  git config --global core.pager \"diff-so-fancy | less --tabs=4 -RFX\"
1065  
1066  # Configure git to use d-s-f for `git add --patch`
1067  git config --global interactive.diffFilter \"diff-so-fancy --patch\"\n";
1068  
1069  	return $out;
1070  }
1071  
1072  sub get_default_colors {
1073  	my $out  = "# Recommended default colors for diff-so-fancy\n";
1074  	$out    .= "# --------------------------------------------\n";
1075  	$out    .= 'git config --global color.ui true
1076  
1077  git config --global color.diff-highlight.oldNormal    "red bold"
1078  git config --global color.diff-highlight.oldHighlight "red bold 52"
1079  git config --global color.diff-highlight.newNormal    "green bold"
1080  git config --global color.diff-highlight.newHighlight "green bold 22"
1081  
1082  git config --global color.diff.meta       "yellow"
1083  git config --global color.diff.frag       "magenta bold"
1084  git config --global color.diff.commit     "yellow bold"
1085  git config --global color.diff.old        "red bold"
1086  git config --global color.diff.new        "green bold"
1087  git config --global color.diff.whitespace "red reverse"
1088  ';
1089  
1090  	return $out;
1091  }
1092  
1093  # Output the current version string
1094  sub version {
1095  	my $ret  = "Diff-so-fancy: https://github.com/so-fancy/diff-so-fancy\n";
1096  	$ret    .= "Version      : $VERSION\n";
1097  
1098  	return $ret;
1099  }
1100  
1101  sub is_windows {
1102  	if ($^O eq 'MSWin32' or $^O eq 'dos' or $^O eq 'os2' or $^O eq 'cygwin' or $^O eq 'msys') {
1103  		return 1;
1104  	} else {
1105  		return 0;
1106  	}
1107  }
1108  
1109  # Return value is whether this is the first time they've run d-s-f
1110  sub check_first_run {
1111  	my $ret = 0;
1112  
1113  	# If first-run is not set, or it's set to "true"
1114  	my $first_run     = git_config_boolean('diff-so-fancy.first-run');
1115  	# See if they're previously set SOME diff-highlight colors
1116  	my $has_dh_colors = git_config_boolean('color.diff-highlight.oldnormal') || git_config_boolean('color.diff-highlight.newnormal');
1117  
1118  	#$first_run = 1; $has_dh_colors = 0;
1119  
1120  	if (!$first_run || $has_dh_colors) {
1121  		return 0;
1122  	} else {
1123  		print "This appears to be the first time you've run diff-so-fancy, please note\n";
1124  		print "that the default git colors are not ideal. Diff-so-fancy recommends the\n";
1125  		print "following colors.\n\n";
1126  
1127  		print get_default_colors();
1128  
1129  		# Set the first run flag to false
1130  		my $cmd = 'git config --global diff-so-fancy.first-run false';
1131  		system($cmd);
1132  
1133  		exit;
1134  	}
1135  
1136  	return 1;
1137  }
1138  
1139  sub set_defaults {
1140  	my $color_config = get_default_colors();
1141  	my $git_config   = 'git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"';
1142  	my $first_cmd    = 'git config --global diff-so-fancy.first-run false';
1143  
1144  	my @cmds = split(/\n/,$color_config);
1145  	push(@cmds,$git_config);
1146  	push(@cmds,$first_cmd);
1147  
1148  	# Remove all comments from the commands
1149  	foreach my $x (@cmds) {
1150  		$x =~ s/#.*//g;
1151  	}
1152  
1153  	# Remove any empty commands
1154  	@cmds = grep($_,@cmds);
1155  
1156  	foreach my $cmd (@cmds) {
1157  		system($cmd);
1158  		my $exit = ($? >> 8);
1159  
1160  		if ($exit != 0) {
1161  			die("Error running: '$cmd' (error #18941)\n");
1162  		}
1163  	}
1164  
1165  	return 1;
1166  }
1167  
1168  # Borrowed from: https://www.perturb.org/display/1167_Perl_ANSI_colors.html
1169  # String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
1170  sub color {
1171  	my $str = shift();
1172  
1173  	# No string sent in, so we just reset
1174  	if (!length($str) || $str eq 'reset') { return "\e[0m"; }
1175  
1176  	# Some predefined colors
1177  	my %color_map = qw(red 160 blue 21 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
1178  	$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
1179  
1180  	# Get foreground/background and any commands
1181  	my ($fc,$cmd) = $str =~ /(\d+)?_?(\w+)?/g;
1182  	my ($bc)      = $str =~ /on_?(\d+)/g;
1183  
1184  	# Some predefined commands
1185  	my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
1186  	my $cmd_num = $cmd_map{$cmd // 0};
1187  
1188  	my $ret = '';
1189  	if ($cmd_num)     { $ret .= "\e[${cmd_num}m"; }
1190  	if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
1191  	if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
1192  
1193  	return $ret;
1194  }
1195  
1196  # Get colors used for various output sections (memoized)
1197  {
1198  	my $static_config;
1199  
1200  	sub get_config_color {
1201  		my $str = shift();
1202  
1203  		my $ret = "";
1204  		if ($static_config->{$str}) {
1205  			return $static_config->{$str};
1206  		}
1207  
1208  		#print color(15) . "Shelling out for color: '$str'\n" . color('reset');
1209  
1210  		if ($str eq "meta") {
1211  			# Default ANSI yellow
1212  			$ret = git_ansi_color(git_config('color.diff.meta')) || color(11);
1213  		} elsif ($str eq "reset") {
1214  			$ret = color("reset");
1215  		} elsif ($str eq "add_line") {
1216  			# Default ANSI green
1217  			$ret = git_ansi_color(git_config('color.diff.new')) || color("2_bold");
1218  		} elsif ($str eq "remove_line") {
1219  			# Default ANSI red
1220  			$ret = git_ansi_color(git_config('color.diff.old')) || color("1_bold");
1221  		} elsif ($str eq "fragment") {
1222  			$ret = git_ansi_color(git_config('color.diff.frag')) || color("13_bold");
1223  		} elsif ($str eq "last_function") {
1224  			$ret = git_ansi_color(git_config('color.diff.func')) || color("146_bold");
1225  		}
1226  
1227  		# Cache (memoize) the entry for later
1228  		$static_config->{$str} = $ret;
1229  
1230  		return $ret;
1231  	}
1232  }
1233  
1234  # https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
1235  sub git_ansi_color {
1236  	my $str   = shift();
1237  	my @parts = split(' ', $str);
1238  
1239  	if (!@parts) {
1240  		return '';
1241  	}
1242  	my $colors = {
1243  		'black'   => 0,
1244  		'red'     => 1,
1245  		'green'   => 2,
1246  		'yellow'  => 3,
1247  		'blue'    => 4,
1248  		'magenta' => 5,
1249  		'cyan'    => 6,
1250  		'white'   => 7,
1251  	};
1252  
1253  	my @ansi_part = ();
1254  
1255  	if (grep { /bold/ } @parts) {
1256  		push(@ansi_part, "1");
1257  		@parts = grep { !/bold/ } @parts; # Remove from array
1258  	}
1259  
1260  	if (grep { /reverse/ } @parts) {
1261  		push(@ansi_part, "7");
1262  		@parts = grep { !/reverse/ } @parts; # Remove from array
1263  	}
1264  
1265  	my $fg  = $parts[0] // "";
1266  	my $bg  = $parts[1] // "";
1267  
1268  	#############################################
1269  
1270  	# It's an numeric value, so it's an 8 bit color
1271  	if (is_numeric($fg)) {
1272  		if ($fg < 8) {
1273  			push(@ansi_part, $fg + 30);
1274  		} elsif ($fg < 16) {
1275  			push(@ansi_part, $fg + 82);
1276  		} else {
1277  			push(@ansi_part, "38;5;$fg");
1278  		}
1279  	# It's a simple 16 color OG ansi
1280  	} elsif ($fg) {
1281  		my $bright    = $fg =~ s/bright//;
1282  		my $color_num = $colors->{$fg} + 30;
1283  
1284  		if ($bright) { $color_num += 60; } # Set bold
1285  
1286  		push(@ansi_part, $color_num);
1287  	}
1288  
1289  	#############################################
1290  
1291  	# It's an numeric value, so it's an 8 bit color
1292  	if (is_numeric($bg)) {
1293  		if ($bg < 8) {
1294  			push(@ansi_part, $bg + 40);
1295  		} elsif ($bg < 16) {
1296  			push(@ansi_part, $bg + 92);
1297  		} else {
1298  			push(@ansi_part, "48;5;$bg");
1299  		}
1300  	# It's a simple 16 color OG ansi
1301  	} elsif ($bg) {
1302  		my $bright    = $bg =~ s/bright//;
1303  		my $color_num = $colors->{$bg} + 40;
1304  
1305  		if ($bright) { $color_num += 60; } # Set bold
1306  
1307  		push(@ansi_part, $color_num);
1308  	}
1309  
1310  	#############################################
1311  
1312  	my $ansi_str = join(";", @ansi_part);
1313  	my $ret      = "\e[" . $ansi_str . "m";
1314  
1315  	return $ret;
1316  }
1317  
1318  sub is_numeric {
1319  	my $s = shift();
1320  
1321  	if ($s =~ /^\d+$/) {
1322  		return 1;
1323  	}
1324  
1325  	return 0;
1326  }
1327  
1328  sub starts_with_ansi {
1329  	my $str = shift();
1330  
1331  	if ($str =~ /^$ansi_color_regex/) {
1332  		return 1;
1333  	} else {
1334  		return 0;
1335  	}
1336  }
1337  
1338  sub get_terminal_width {
1339  	# Make width static so we only calculate it once
1340  	state $width;
1341  
1342  	if ($width) {
1343  		return $width;
1344  	}
1345  
1346  	# If there is a ruler width in the config we use that
1347  	if ($ruler_width) {
1348  		$width = $ruler_width;
1349  	# Otherwise we check the terminal width using tput
1350  	} else {
1351  		my $tput = `tput cols`;
1352  
1353  		if ($tput) {
1354  			$width = int($tput);
1355  
1356  			if (is_windows()) {
1357  				$width--;
1358  			}
1359  		} else {
1360  			print color('orange') . "Warning: `tput cols` did not return numeric input" . color('reset') . "\n";
1361  			$width = 80;
1362  		}
1363  	}
1364  
1365  	return $width;
1366  }
1367  
1368  sub show_debug_info {
1369  	my @less    = get_less_charset();
1370  	my $git_ver = trim(`git --version`);
1371  	$git_ver    =~ s/[^\d.]//g;
1372  
1373  	print "Diff-so-fancy   : v$VERSION\n";
1374  	print "Git             : v$git_ver\n";
1375  	print "Perl            : $^V\n";
1376  	print "\n";
1377  
1378  	print "Terminal width  : " . get_terminal_width() . "\n";
1379  	print "Terminal \$LANG  : " . ($ENV{LANG} || "") . "\n";
1380  	print "\n";
1381  	print "Supports Unicode: " . yes_no(should_print_unicode()) . "\n";
1382  	print "Unicode Ruler   : " . yes_no($use_unicode_dash_for_ruler) . "\n";
1383  	print "\n";
1384  	print "Less Charset Var: " . ($less[0] // "") . "\n";
1385  	print "Less Charset    : " . ($less[1] // "") . "\n";
1386  	print "\n";
1387  	print "Is Windows      : " . yes_no(is_windows()) . "\n";
1388  	print "Operating System: $^O\n";
1389  }
1390  
1391  sub yes_no {
1392  	my $val = shift();
1393  
1394  	if ($val) {
1395  		return "Yes";
1396  	} else {
1397  		return "No";
1398  	}
1399  }
1400  
1401  # If there are colors set in the gitconfig use those, otherwise leave the defaults
1402  sub init_diff_highlight_colors {
1403  	$DiffHighlight::NEW_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.newnormal'))    || $DiffHighlight::NEW_HIGHLIGHT[0];
1404  	$DiffHighlight::NEW_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.newhighlight')) || $DiffHighlight::NEW_HIGHLIGHT[1];
1405  
1406  	$DiffHighlight::OLD_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.oldnormal'))    || $DiffHighlight::OLD_HIGHLIGHT[0];
1407  	$DiffHighlight::OLD_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.oldhighlight')) || $DiffHighlight::OLD_HIGHLIGHT[1];
1408  }
1409  
1410  sub debug_log {
1411  	my $log_line = shift();
1412  	my $file     = "/tmp/diff-so-fancy.debug.log";
1413  
1414  	state $fh;
1415  	if (!$fh) {
1416  		printf("%sDebug log enabled:%s $file\n", color('orange'), color());
1417  		open ($fh, ">", $file) or die("Cannot write to $file");
1418  	}
1419  
1420  	print $fh trim($log_line) . "\n";
1421  
1422  	return 1;
1423  }
1424  
1425  # Enable k() and kd() if there is a DSF_DEBUG environment variable
1426  BEGIN {
1427  	if ($ENV{"DSF_DEBUG"}) {
1428  		require Data::Dump::Color;
1429  		*k  = sub { Data::Dump::Color::dd(@_) };
1430  		*kd = sub {
1431  			k(@_);
1432  
1433  			printf("Died at %2\$s line #%3\$s\n",caller());
1434  			exit(15);
1435  		}
1436  	} else {
1437  		*k  = sub {};
1438  		*kd = sub {};
1439  	}
1440  }
1441  
1442  # vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4