diff-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: diff-cli
3
+ Version: 0.1.0
4
+ Summary: File and text differ with multiple output formats
5
+ Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/diff-cli
6
+ Project-URL: Repository, https://github.com/marcusbuildsthings-droid/diff-cli
7
+ Author-email: Marcus <marcus.builds.things@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,compare,diff,side-by-side,text,unified
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Text Processing :: General
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: click>=8.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # diff-cli
27
+
28
+ File and text differ with multiple output formats.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install diff-cli
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ # Compare files
40
+ diff-cli files file1.txt file2.txt
41
+
42
+ # Compare text strings
43
+ diff-cli text "hello world" "hello there"
44
+
45
+ # Compare directories
46
+ diff-cli dirs dir1/ dir2/
47
+
48
+ # Output formats
49
+ diff-cli files a.txt b.txt --unified # Default
50
+ diff-cli files a.txt b.txt --side-by-side # Two-column
51
+ diff-cli files a.txt b.txt --context # Context format
52
+ diff-cli files a.txt b.txt --minimal # Changes only
53
+ diff-cli files a.txt b.txt --json # JSON for automation
54
+
55
+ # Options
56
+ diff-cli files a.txt b.txt -n 5 # 5 context lines
57
+ diff-cli files a.txt b.txt --ignore-whitespace
58
+ diff-cli files a.txt b.txt --ignore-case
59
+ diff-cli files a.txt b.txt --ignore-blank-lines
60
+
61
+ # Watch mode
62
+ diff-cli watch file1.txt file2.txt
63
+ ```
64
+
65
+ ## Exit Codes
66
+
67
+ | Code | Meaning |
68
+ |------|---------|
69
+ | 0 | Files are identical |
70
+ | 1 | Files differ |
71
+ | 2 | Error (file not found, etc.) |
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,7 @@
1
+ diffcli/__init__.py,sha256=iLGalpEvIHOEaUKrqInA-4WTvvoKanHxP50mI6vf9VQ,89
2
+ diffcli/cli.py,sha256=9f2w6U-Fqol0Cr6aDZEWbPxRCb53LrGvtU-Ci4C8oPc,18218
3
+ diff_cli-0.1.0.dist-info/METADATA,sha256=xZ3D6WNa7a2t3M4XjlbWgfi4RSG6AOzIfKC_Ma5f5RQ,2043
4
+ diff_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ diff_cli-0.1.0.dist-info/entry_points.txt,sha256=s6vMqHwxJ90b7Q68i8Sp8F-G_A9MhITztu7JOEESAEA,45
6
+ diff_cli-0.1.0.dist-info/licenses/LICENSE,sha256=9tNBpWq8KGbuJqmeComp40OiNnbvpvsKn1YP26PUtck,1063
7
+ diff_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diff-cli = diffcli.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
diffcli/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """diff-cli: File and text differ with multiple output formats."""
2
+ __version__ = "0.1.0"
diffcli/cli.py ADDED
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env python3
2
+ """diff-cli: File and text differ with multiple output formats."""
3
+
4
+ import os
5
+ import sys
6
+ import json
7
+ import time
8
+ import difflib
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ import click
14
+
15
+
16
+ def colorize_diff_line(line: str, color_enabled: bool = True) -> str:
17
+ """Add color to diff lines if color is enabled."""
18
+ if not color_enabled or not sys.stdout.isatty():
19
+ return line
20
+
21
+ if line.startswith('+++') or line.startswith('---'):
22
+ return click.style(line, fg='white', bold=True)
23
+ elif line.startswith('@@'):
24
+ return click.style(line, fg='cyan', bold=True)
25
+ elif line.startswith('+'):
26
+ return click.style(line, fg='green')
27
+ elif line.startswith('-'):
28
+ return click.style(line, fg='red')
29
+ elif line.startswith('?'):
30
+ return click.style(line, fg='yellow')
31
+ else:
32
+ return line
33
+
34
+
35
+ def normalize_text(text: str, ignore_whitespace: bool = False,
36
+ ignore_case: bool = False, ignore_blank_lines: bool = False) -> str:
37
+ """Normalize text according to ignore options."""
38
+ if ignore_case:
39
+ text = text.lower()
40
+
41
+ if ignore_blank_lines:
42
+ lines = [line for line in text.split('\n') if line.strip()]
43
+ text = '\n'.join(lines)
44
+
45
+ if ignore_whitespace:
46
+ # Replace multiple whitespace with single space, strip line ends
47
+ lines = [' '.join(line.split()) for line in text.split('\n')]
48
+ text = '\n'.join(lines)
49
+
50
+ return text
51
+
52
+
53
+ def files_are_identical(file1: Path, file2: Path, ignore_whitespace: bool = False,
54
+ ignore_case: bool = False, ignore_blank_lines: bool = False) -> bool:
55
+ """Check if two files are identical after normalization."""
56
+ try:
57
+ with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
58
+ text1 = normalize_text(f1.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
59
+ text2 = normalize_text(f2.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
60
+ return text1 == text2
61
+ except UnicodeDecodeError:
62
+ # Binary comparison for non-text files
63
+ with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
64
+ return f1.read() == f2.read()
65
+
66
+
67
+ def unified_diff(text1: str, text2: str, filename1: str = 'file1',
68
+ filename2: str = 'file2', context: int = 3) -> List[str]:
69
+ """Generate unified diff between two texts."""
70
+ lines1 = text1.splitlines(keepends=True)
71
+ lines2 = text2.splitlines(keepends=True)
72
+
73
+ return list(difflib.unified_diff(
74
+ lines1, lines2,
75
+ fromfile=filename1,
76
+ tofile=filename2,
77
+ n=context
78
+ ))
79
+
80
+
81
+ def side_by_side_diff(text1: str, text2: str, width: int = 80) -> List[str]:
82
+ """Generate side-by-side diff between two texts."""
83
+ lines1 = text1.splitlines()
84
+ lines2 = text2.splitlines()
85
+
86
+ max_len = max(len(lines1), len(lines2))
87
+ col_width = width // 2 - 2
88
+
89
+ result = []
90
+ result.append(f"{'File 1':<{col_width}} | {'File 2':<{col_width}}")
91
+ result.append(f"{'-' * col_width} | {'-' * col_width}")
92
+
93
+ for i in range(max_len):
94
+ left = lines1[i] if i < len(lines1) else ""
95
+ right = lines2[i] if i < len(lines2) else ""
96
+
97
+ # Truncate long lines
98
+ if len(left) > col_width:
99
+ left = left[:col_width-3] + "..."
100
+ if len(right) > col_width:
101
+ right = right[:col_width-3] + "..."
102
+
103
+ # Determine line status
104
+ if i >= len(lines1):
105
+ marker = "+"
106
+ elif i >= len(lines2):
107
+ marker = "-"
108
+ elif left != right:
109
+ marker = "!"
110
+ else:
111
+ marker = " "
112
+
113
+ result.append(f"{left:<{col_width}} {marker} {right:<{col_width}}")
114
+
115
+ return result
116
+
117
+
118
+ def context_diff(text1: str, text2: str, filename1: str = 'file1',
119
+ filename2: str = 'file2', context: int = 3) -> List[str]:
120
+ """Generate context diff between two texts."""
121
+ lines1 = text1.splitlines(keepends=True)
122
+ lines2 = text2.splitlines(keepends=True)
123
+
124
+ return list(difflib.context_diff(
125
+ lines1, lines2,
126
+ fromfile=filename1,
127
+ tofile=filename2,
128
+ n=context
129
+ ))
130
+
131
+
132
+ def minimal_diff(text1: str, text2: str) -> Dict[str, Any]:
133
+ """Generate minimal diff summary."""
134
+ lines1 = text1.splitlines()
135
+ lines2 = text2.splitlines()
136
+
137
+ if lines1 == lines2:
138
+ return {
139
+ "identical": True,
140
+ "changes": 0,
141
+ "additions": 0,
142
+ "deletions": 0
143
+ }
144
+
145
+ # Use difflib to get a basic comparison
146
+ diff = list(difflib.unified_diff(lines1, lines2, n=0))
147
+
148
+ additions = sum(1 for line in diff if line.startswith('+') and not line.startswith('+++'))
149
+ deletions = sum(1 for line in diff if line.startswith('-') and not line.startswith('---'))
150
+
151
+ return {
152
+ "identical": False,
153
+ "changes": additions + deletions,
154
+ "additions": additions,
155
+ "deletions": deletions,
156
+ "total_lines_1": len(lines1),
157
+ "total_lines_2": len(lines2)
158
+ }
159
+
160
+
161
+ def scan_directory(directory: Path, recursive: bool = True) -> List[Path]:
162
+ """Scan directory for files."""
163
+ files = []
164
+ if recursive:
165
+ for root, dirs, filenames in os.walk(directory):
166
+ for filename in filenames:
167
+ files.append(Path(root) / filename)
168
+ else:
169
+ for item in directory.iterdir():
170
+ if item.is_file():
171
+ files.append(item)
172
+ return sorted(files)
173
+
174
+
175
+ def compare_directories(dir1: Path, dir2: Path, recursive: bool = True) -> Dict[str, Any]:
176
+ """Compare two directories."""
177
+ files1 = {f.relative_to(dir1): f for f in scan_directory(dir1, recursive)}
178
+ files2 = {f.relative_to(dir2): f for f in scan_directory(dir2, recursive)}
179
+
180
+ all_files = set(files1.keys()) | set(files2.keys())
181
+ only_in_1 = set(files1.keys()) - set(files2.keys())
182
+ only_in_2 = set(files2.keys()) - set(files1.keys())
183
+ common_files = set(files1.keys()) & set(files2.keys())
184
+
185
+ different_files = []
186
+ identical_files = []
187
+
188
+ for rel_path in common_files:
189
+ file1 = files1[rel_path]
190
+ file2 = files2[rel_path]
191
+
192
+ if files_are_identical(file1, file2):
193
+ identical_files.append(str(rel_path))
194
+ else:
195
+ different_files.append(str(rel_path))
196
+
197
+ return {
198
+ "total_files": len(all_files),
199
+ "only_in_first": sorted([str(f) for f in only_in_1]),
200
+ "only_in_second": sorted([str(f) for f in only_in_2]),
201
+ "different": sorted(different_files),
202
+ "identical": sorted(identical_files),
203
+ "summary": {
204
+ "only_in_first": len(only_in_1),
205
+ "only_in_second": len(only_in_2),
206
+ "different": len(different_files),
207
+ "identical": len(identical_files)
208
+ }
209
+ }
210
+
211
+
212
+ @click.group()
213
+ @click.version_option(version="0.1.0", prog_name="diff-cli")
214
+ def cli():
215
+ """diff-cli: File and text differ with multiple output formats."""
216
+ pass
217
+
218
+
219
+ @cli.command()
220
+ @click.argument('file1', type=click.Path(exists=True, path_type=Path))
221
+ @click.argument('file2', type=click.Path(exists=True, path_type=Path))
222
+ @click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
223
+ default='unified', help='Diff output format')
224
+ @click.option('--context', '-n', default=3, help='Number of context lines')
225
+ @click.option('--ignore-whitespace', '-w', is_flag=True, help='Ignore whitespace differences')
226
+ @click.option('--ignore-case', '-i', is_flag=True, help='Ignore case differences')
227
+ @click.option('--ignore-blank-lines', '-B', is_flag=True, help='Ignore blank lines')
228
+ @click.option('--no-color', is_flag=True, help='Disable colored output')
229
+ @click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
230
+ def files(file1: Path, file2: Path, format: str, context: int,
231
+ ignore_whitespace: bool, ignore_case: bool, ignore_blank_lines: bool,
232
+ no_color: bool, output_json: bool):
233
+ """Compare two files."""
234
+ try:
235
+ with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
236
+ text1 = normalize_text(f1.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
237
+ text2 = normalize_text(f2.read(), ignore_whitespace, ignore_case, ignore_blank_lines)
238
+ except UnicodeDecodeError:
239
+ if output_json:
240
+ click.echo(json.dumps({"error": "Binary files cannot be compared in text mode", "exit_code": 2}))
241
+ else:
242
+ click.echo("Error: Binary files cannot be compared in text mode", err=True)
243
+ sys.exit(2)
244
+
245
+ # Check if files are identical
246
+ identical = text1 == text2
247
+
248
+ if output_json:
249
+ result = {
250
+ "files": [str(file1), str(file2)],
251
+ "identical": identical,
252
+ "format": format
253
+ }
254
+
255
+ if format == 'minimal':
256
+ result.update(minimal_diff(text1, text2))
257
+ else:
258
+ if format == 'unified':
259
+ diff_lines = unified_diff(text1, text2, str(file1), str(file2), context)
260
+ elif format == 'side-by-side':
261
+ diff_lines = side_by_side_diff(text1, text2)
262
+ elif format == 'context':
263
+ diff_lines = context_diff(text1, text2, str(file1), str(file2), context)
264
+
265
+ result["diff"] = diff_lines
266
+
267
+ click.echo(json.dumps(result, indent=2))
268
+ else:
269
+ if identical:
270
+ click.echo("Files are identical")
271
+ else:
272
+ if format == 'minimal':
273
+ summary = minimal_diff(text1, text2)
274
+ click.echo(f"Files differ: {summary['changes']} changes ({summary['additions']} additions, {summary['deletions']} deletions)")
275
+ else:
276
+ if format == 'unified':
277
+ diff_lines = unified_diff(text1, text2, str(file1), str(file2), context)
278
+ elif format == 'side-by-side':
279
+ diff_lines = side_by_side_diff(text1, text2)
280
+ elif format == 'context':
281
+ diff_lines = context_diff(text1, text2, str(file1), str(file2), context)
282
+
283
+ color_enabled = not no_color
284
+ for line in diff_lines:
285
+ click.echo(colorize_diff_line(line.rstrip(), color_enabled))
286
+
287
+ sys.exit(0 if identical else 1)
288
+
289
+
290
+ @cli.command()
291
+ @click.argument('text1')
292
+ @click.argument('text2')
293
+ @click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
294
+ default='unified', help='Diff output format')
295
+ @click.option('--context', '-n', default=3, help='Number of context lines')
296
+ @click.option('--ignore-whitespace', '-w', is_flag=True, help='Ignore whitespace differences')
297
+ @click.option('--ignore-case', '-i', is_flag=True, help='Ignore case differences')
298
+ @click.option('--ignore-blank-lines', '-B', is_flag=True, help='Ignore blank lines')
299
+ @click.option('--no-color', is_flag=True, help='Disable colored output')
300
+ @click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
301
+ def text(text1: str, text2: str, format: str, context: int,
302
+ ignore_whitespace: bool, ignore_case: bool, ignore_blank_lines: bool,
303
+ no_color: bool, output_json: bool):
304
+ """Compare two text strings."""
305
+ normalized_text1 = normalize_text(text1, ignore_whitespace, ignore_case, ignore_blank_lines)
306
+ normalized_text2 = normalize_text(text2, ignore_whitespace, ignore_case, ignore_blank_lines)
307
+
308
+ identical = normalized_text1 == normalized_text2
309
+
310
+ if output_json:
311
+ result = {
312
+ "texts": [text1, text2],
313
+ "identical": identical,
314
+ "format": format
315
+ }
316
+
317
+ if format == 'minimal':
318
+ result.update(minimal_diff(normalized_text1, normalized_text2))
319
+ else:
320
+ if format == 'unified':
321
+ diff_lines = unified_diff(normalized_text1, normalized_text2, "text1", "text2", context)
322
+ elif format == 'side-by-side':
323
+ diff_lines = side_by_side_diff(normalized_text1, normalized_text2)
324
+ elif format == 'context':
325
+ diff_lines = context_diff(normalized_text1, normalized_text2, "text1", "text2", context)
326
+
327
+ result["diff"] = diff_lines
328
+
329
+ click.echo(json.dumps(result, indent=2))
330
+ else:
331
+ if identical:
332
+ click.echo("Texts are identical")
333
+ else:
334
+ if format == 'minimal':
335
+ summary = minimal_diff(normalized_text1, normalized_text2)
336
+ click.echo(f"Texts differ: {summary['changes']} changes ({summary['additions']} additions, {summary['deletions']} deletions)")
337
+ else:
338
+ if format == 'unified':
339
+ diff_lines = unified_diff(normalized_text1, normalized_text2, "text1", "text2", context)
340
+ elif format == 'side-by-side':
341
+ diff_lines = side_by_side_diff(normalized_text1, normalized_text2)
342
+ elif format == 'context':
343
+ diff_lines = context_diff(normalized_text1, normalized_text2, "text1", "text2", context)
344
+
345
+ color_enabled = not no_color
346
+ for line in diff_lines:
347
+ click.echo(colorize_diff_line(line.rstrip(), color_enabled))
348
+
349
+ sys.exit(0 if identical else 1)
350
+
351
+
352
+ @cli.command()
353
+ @click.argument('dir1', type=click.Path(exists=True, file_okay=False, path_type=Path))
354
+ @click.argument('dir2', type=click.Path(exists=True, file_okay=False, path_type=Path))
355
+ @click.option('--recursive', '-r', is_flag=True, default=True, help='Compare directories recursively')
356
+ @click.option('--json', 'output_json', is_flag=True, help='Output in JSON format')
357
+ def dirs(dir1: Path, dir2: Path, recursive: bool, output_json: bool):
358
+ """Compare two directories."""
359
+ result = compare_directories(dir1, dir2, recursive)
360
+
361
+ if output_json:
362
+ click.echo(json.dumps(result, indent=2))
363
+ else:
364
+ summary = result["summary"]
365
+ click.echo(f"Directory comparison: {dir1} vs {dir2}")
366
+ click.echo(f"Total files: {result['total_files']}")
367
+ click.echo(f"Only in first: {summary['only_in_first']}")
368
+ click.echo(f"Only in second: {summary['only_in_second']}")
369
+ click.echo(f"Different: {summary['different']}")
370
+ click.echo(f"Identical: {summary['identical']}")
371
+
372
+ if result['only_in_first']:
373
+ click.echo(f"\nFiles only in {dir1}:")
374
+ for f in result['only_in_first']:
375
+ click.echo(f" - {f}")
376
+
377
+ if result['only_in_second']:
378
+ click.echo(f"\nFiles only in {dir2}:")
379
+ for f in result['only_in_second']:
380
+ click.echo(f" + {f}")
381
+
382
+ if result['different']:
383
+ click.echo(f"\nDifferent files:")
384
+ for f in result['different']:
385
+ click.echo(f" ! {f}")
386
+
387
+ # Exit with 1 if there are any differences
388
+ has_differences = (summary['only_in_first'] > 0 or
389
+ summary['only_in_second'] > 0 or
390
+ summary['different'] > 0)
391
+ sys.exit(1 if has_differences else 0)
392
+
393
+
394
+ @cli.command()
395
+ @click.argument('file1', type=click.Path(exists=True, path_type=Path))
396
+ @click.argument('file2', type=click.Path(exists=True, path_type=Path))
397
+ @click.option('--interval', '-i', default=1.0, help='Check interval in seconds')
398
+ @click.option('--format', '-f', type=click.Choice(['unified', 'side-by-side', 'context', 'minimal']),
399
+ default='unified', help='Diff output format')
400
+ @click.option('--no-color', is_flag=True, help='Disable colored output')
401
+ def watch(file1: Path, file2: Path, interval: float, format: str, no_color: bool):
402
+ """Watch two files for changes and show diff when they change."""
403
+ click.echo(f"Watching {file1} and {file2} for changes (Ctrl+C to stop)...")
404
+
405
+ last_mtime1 = file1.stat().st_mtime if file1.exists() else 0
406
+ last_mtime2 = file2.stat().st_mtime if file2.exists() else 0
407
+
408
+ try:
409
+ while True:
410
+ time.sleep(interval)
411
+
412
+ current_mtime1 = file1.stat().st_mtime if file1.exists() else 0
413
+ current_mtime2 = file2.stat().st_mtime if file2.exists() else 0
414
+
415
+ if current_mtime1 != last_mtime1 or current_mtime2 != last_mtime2:
416
+ click.echo(f"\n--- Change detected at {time.strftime('%Y-%m-%d %H:%M:%S')} ---")
417
+
418
+ try:
419
+ with open(file1, 'r', encoding='utf-8') as f1, open(file2, 'r', encoding='utf-8') as f2:
420
+ text1 = f1.read()
421
+ text2 = f2.read()
422
+
423
+ if text1 == text2:
424
+ click.echo("Files are now identical")
425
+ else:
426
+ if format == 'minimal':
427
+ summary = minimal_diff(text1, text2)
428
+ click.echo(f"Files differ: {summary['changes']} changes")
429
+ else:
430
+ if format == 'unified':
431
+ diff_lines = unified_diff(text1, text2, str(file1), str(file2))
432
+ elif format == 'side-by-side':
433
+ diff_lines = side_by_side_diff(text1, text2)
434
+ elif format == 'context':
435
+ diff_lines = context_diff(text1, text2, str(file1), str(file2))
436
+
437
+ color_enabled = not no_color
438
+ for line in diff_lines:
439
+ click.echo(colorize_diff_line(line.rstrip(), color_enabled))
440
+
441
+ except Exception as e:
442
+ click.echo(f"Error reading files: {e}", err=True)
443
+
444
+ last_mtime1 = current_mtime1
445
+ last_mtime2 = current_mtime2
446
+
447
+ except KeyboardInterrupt:
448
+ click.echo("\nStopped watching.")
449
+
450
+
451
+ if __name__ == '__main__':
452
+ cli()