|
4 | 4 | import shutil |
5 | 5 | import sys |
6 | 6 | import tomllib |
| 7 | +from collections import Counter |
| 8 | +from enum import Enum |
7 | 9 | from pathlib import Path |
8 | 10 | from typing import Iterable |
9 | 11 | from typing import Sequence |
@@ -94,17 +96,34 @@ def _resolve_dependencies(entry_files: Sequence[Path]) -> list[Path]: |
94 | 96 | return internals + components |
95 | 97 |
|
96 | 98 |
|
97 | | -def _copy_file(src: Path, dest_dir: Path, force: bool) -> Path: |
| 99 | +class CopyStatus(Enum): |
| 100 | + COPIED = "copied" |
| 101 | + SKIPPED_UNCHANGED = "skipped_unchanged" |
| 102 | + SKIPPED_USER = "skipped_user" |
| 103 | + |
| 104 | + |
| 105 | +def _copy_file( |
| 106 | + src: Path, |
| 107 | + dest_dir: Path, |
| 108 | + force: bool, |
| 109 | + dest_name: str | None = None, |
| 110 | +) -> tuple[Path, CopyStatus]: |
98 | 111 | dest_dir.mkdir(parents=True, exist_ok=True) |
99 | | - dest = dest_dir / src.name |
100 | | - if dest.exists() and not force: |
101 | | - overwrite = questionary.confirm(f"{dest} exists. Overwrite?", default=False).ask() |
102 | | - if not overwrite: |
103 | | - click.echo(f"Skipped {dest}") |
104 | | - return dest |
| 112 | + name = dest_name or src.name |
| 113 | + dest = dest_dir / name |
| 114 | + if dest.exists(): |
| 115 | + src_bytes = src.read_bytes() |
| 116 | + dest_bytes = dest.read_bytes() |
| 117 | + if dest_bytes == src_bytes: |
| 118 | + return dest, CopyStatus.SKIPPED_UNCHANGED |
| 119 | + if not force: |
| 120 | + overwrite = questionary.confirm(f"{dest} exists. Overwrite?", default=False).ask() |
| 121 | + if not overwrite: |
| 122 | + click.echo(f"Skipped {dest}") |
| 123 | + return dest, CopyStatus.SKIPPED_USER |
105 | 124 | shutil.copy2(src, dest) |
106 | 125 | click.echo(f"Copied {src.name} -> {dest}") |
107 | | - return dest |
| 126 | + return dest, CopyStatus.COPIED |
108 | 127 |
|
109 | 128 |
|
110 | 129 | def _find_pyproject(start: Path | None = None) -> Path | None: |
@@ -252,8 +271,27 @@ def add_cmd(components: tuple[str, ...], dest: Path | None, select_all: bool, fo |
252 | 271 | init_py = dest / "__init__.py" |
253 | 272 | if not init_py.exists(): |
254 | 273 | _write_text(init_py, "") |
| 274 | + status_counts: Counter[CopyStatus] = Counter() |
255 | 275 | for src in files: |
256 | | - _copy_file(src, dest, force=force) |
| 276 | + _, status = _copy_file(src, dest, force=force) |
| 277 | + status_counts[status] += 1 |
| 278 | + |
| 279 | + copied = status_counts[CopyStatus.COPIED] |
| 280 | + if copied: |
| 281 | + suffix = "s" if copied != 1 else "" |
| 282 | + click.echo(f"\nCopied {copied} file{suffix}.") |
| 283 | + |
| 284 | + summaries: list[str] = [] |
| 285 | + unchanged = status_counts[CopyStatus.SKIPPED_UNCHANGED] |
| 286 | + if unchanged: |
| 287 | + suffix = "s" if unchanged != 1 else "" |
| 288 | + summaries.append(f"{unchanged} file{suffix} already up-to-date") |
| 289 | + skipped = status_counts[CopyStatus.SKIPPED_USER] |
| 290 | + if skipped: |
| 291 | + suffix = "s" if skipped != 1 else "" |
| 292 | + summaries.append(f"{skipped} file{suffix} overwrite declined") |
| 293 | + if summaries: |
| 294 | + click.echo(f"Skipped {'; '.join(summaries)}") |
257 | 295 |
|
258 | 296 | click.echo("\nDone. Remember to run your Tailwind build.") |
259 | 297 |
|
@@ -295,13 +333,14 @@ def add_theme_cmd(theme: str | None, dest: Path | None, force: bool) -> None: |
295 | 333 | if dest is None: |
296 | 334 | dest = default_dest |
297 | 335 |
|
298 | | - if dest.exists() and not force: |
299 | | - if not questionary.confirm(f"{dest} exists. Overwrite?", default=False).ask(): |
300 | | - click.echo("Skipped theme.") |
301 | | - return |
302 | | - content = _read_text(src) |
303 | | - _write_text(dest, content) |
304 | | - click.echo(f"Copied {src.name} -> {dest}") |
| 336 | + _, status = _copy_file( |
| 337 | + src, |
| 338 | + dest.parent, |
| 339 | + force=force, |
| 340 | + dest_name=dest.name, |
| 341 | + ) |
| 342 | + if status != CopyStatus.COPIED: |
| 343 | + return |
305 | 344 |
|
306 | 345 |
|
307 | 346 | @cli.command("themes") |
|
0 commit comments