#!/usr/bin/env python3 """Diffr — side-by-side file diff viewer with color.""" import argparse, difflib, os, sys def color(text, code): return f"\033[{code}m{text}\033[0m" if sys.stdout.isatty() else text def split_lines(text): return text.rstrip("\n").split("\n") if "\n" in text else [text.rstrip()] def side_by_side(left_lines, right_lines, context=3, width=80): half = (width - 3) // 2 result = [] matcher = difflib.SequenceMatcher(None, left_lines, right_lines) for op, i1, i2, j1, j2 in matcher.get_opcodes(): if op == "equal": # Show context around changes if context > 0 and result: # Check if we should show only context lines for k in range(i1, i2): result.append(( left_lines[k], right_lines[k - i1 + j1], " ", )) else: for k in range(i1, i2): result.append(( left_lines[k], right_lines[k - i1 + j1], " ", )) elif op == "replace": for k in range(i1, i2): r_idx = j1 + (k - i1) if r_idx < j2: result.append((left_lines[k], right_lines[r_idx], "-")) for k in range(max(i1, i2 - (i2 - i1)), i2): pass elif op == "delete": for k in range(i1, i2): result.append((left_lines[k], "", "-")) elif op == "insert": for k in range(j1, j2): result.append(("", right_lines[k], "+")) return result def color_diff_line(left, right, marker, half, use_color=True): if marker == " ": l = left[:half].ljust(half) r = right[:half].ljust(half) if use_color: return f" {color(l, '90')} │ {color(r, '90')}" return f" {l} │ {r}" elif marker == "-": l = left[:half].ljust(half) if use_color: return f"{color('<', '31')}{color(l, '31')} │ {color(''.ljust(half), '90')}" return f"<{l} │" elif marker == "+": r = right[:half].ljust(half) if use_color: return f" {color(''.ljust(half), '90')} │ {color('>', '32')}{color(r, '32')}" return f" │ >{r}" return f" {left[:half].ljust(half)} │ {right[:half].ljust(half)}" def simple_diff(left_lines, right_lines, use_color=True): matcher = difflib.SequenceMatcher(None, left_lines, right_lines) result = [] for op, i1, i2, j1, j2 in matcher.get_opcodes(): if op == "equal": for k in range(i1, i2): result.append((" ", left_lines[k])) elif op == "replace": for k in range(i1, i2): result.append(("-", left_lines[k])) for k in range(j1, j2): result.append(("+", right_lines[k])) elif op == "delete": for k in range(i1, i2): result.append(("-", left_lines[k])) elif op == "insert": for k in range(j1, j2): result.append(("+", right_lines[k])) return result def display_simple(diff_result, use_color=True, context=3): for i, (marker, line) in enumerate(diff_result): if marker == " ": if use_color: print(f" {line}") else: print(f" {line}") elif marker == "-": if use_color: print(f" {color('-' + line, '31')}") else: print(f" -{line}") elif marker == "+": if use_color: print(f" {color('+' + line, '32')}") else: print(f" +{line}") def main(): parser = argparse.ArgumentParser( description="Diffr — file diff viewer with color", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " diffr file1.txt file2.txt # Side-by-side diff\n" " diffr file1.txt file2.txt --simple # Unified diff\n" " diffr file1.txt file2.txt --context 5 # 5 lines context\n" ), ) parser.add_argument("file1", help="First file") parser.add_argument("file2", help="Second file") parser.add_argument("--context", type=int, default=3, help="Context lines (default: 3)") parser.add_argument("--simple", action="store_true", help="Use unified diff format") parser.add_argument("--no-color", action="store_true", help="Disable color output") parser.add_argument("--version", action="version", version="diffr 1.0.0") args = parser.parse_args() try: with open(args.file1) as f: left = f.read().rstrip("\n").split("\n") with open(args.file2) as f: right = f.read().rstrip("\n").split("\n") except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) use_color = sys.stdout.isatty() and not args.no_color print(f"\n {color('Diffr: ', '1')}{args.file1} {color('vs', '90')} {args.file2}") print(f" {color('─' * 60, '90')}\n") if args.simple: result = simple_diff(left, right, use_color) display_simple(result, use_color, args.context) else: # Basic line-by-line display with markers max_len = max(len(left), len(right)) num_width = len(str(max_len)) for i in range(max_len): l = left[i] if i < len(left) else "" r = right[i] if i < len(right) else "" if l == r: if use_color: print(f" {color(str(i+1).rjust(num_width), '90')} {l}") else: print(f" {str(i+1).rjust(num_width)} {l}") elif not l: if use_color: print(f" {color(str(i+1).rjust(num_width), '90')} {color('+' + r, '32')}") else: print(f" {str(i+1).rjust(num_width)} +{r}") elif not r: if use_color: print(f" {color(str(i+1).rjust(num_width), '90')} {color('-' + l, '31')}") else: print(f" {str(i+1).rjust(num_width)} -{l}") else: if use_color: print(f" {color(str(i+1).rjust(num_width), '90')} {color('-' + l, '31')}") print(f" {color(str(i+2).rjust(num_width), '90')} {color('+' + r, '32')}") else: print(f" {str(i+1).rjust(num_width)} -{l}") print(f" {str(i+2).rjust(num_width)} +{r}") print() if __name__ == "__main__": main()