dotx 2.0.4__tar.gz → 2.1.0__tar.gz
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.
- {dotx-2.0.4/src/dotx.egg-info → dotx-2.1.0}/PKG-INFO +2 -1
- {dotx-2.0.4 → dotx-2.1.0}/pyproject.toml +2 -1
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/cli.py +245 -67
- {dotx-2.0.4 → dotx-2.1.0/src/dotx.egg-info}/PKG-INFO +2 -1
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/requires.txt +1 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_cli_database.py +3 -2
- {dotx-2.0.4 → dotx-2.1.0}/LICENSE +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/MANIFEST.in +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/README.md +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/setup.cfg +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/__init__.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/database.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/ignore.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/install.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/installed-schema.sql +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/options.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/plan.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx/uninstall.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/SOURCES.txt +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/dependency_links.txt +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/entry_points.txt +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/src/dotx.egg-info/top_level.txt +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_cli.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_ignore.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_ignore_rules.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_install.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_options.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_plan.py +0 -0
- {dotx-2.0.4 → dotx-2.1.0}/tests/test_uninstall.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotx
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A command-line tool to install a link-farm to your dotfiles
|
|
5
5
|
Author-email: Wolf <Wolf@zv.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: click>=8.1.7
|
|
23
23
|
Requires-Dist: loguru>=0.7.0
|
|
24
24
|
Requires-Dist: pathspec>=0.12.1
|
|
25
|
+
Requires-Dist: rich>=13.9.4
|
|
25
26
|
Requires-Dist: typer>=0.15.1
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dotx"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "A command-line tool to install a link-farm to your dotfiles"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Wolf", email = "Wolf@zv.cx" }
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"click>=8.1.7",
|
|
25
25
|
"loguru>=0.7.0",
|
|
26
26
|
"pathspec>=0.12.1",
|
|
27
|
+
"rich>=13.9.4",
|
|
27
28
|
"typer>=0.15.1",
|
|
28
29
|
]
|
|
29
30
|
|
|
@@ -21,11 +21,15 @@ from typing import Annotated, Optional
|
|
|
21
21
|
import click
|
|
22
22
|
import typer
|
|
23
23
|
from loguru import logger
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.panel import Panel
|
|
24
28
|
|
|
25
29
|
from dotx import __version__, __homepage__
|
|
26
30
|
from dotx.database import InstallationDB
|
|
27
31
|
from dotx.install import plan_install
|
|
28
|
-
from dotx.options import set_option
|
|
32
|
+
from dotx.options import set_option, is_verbose_mode
|
|
29
33
|
from dotx.plan import Action, Plan, execute_plan, extract_plan, log_extracted_plan
|
|
30
34
|
from dotx.uninstall import plan_uninstall
|
|
31
35
|
|
|
@@ -128,6 +132,8 @@ def install(
|
|
|
128
132
|
):
|
|
129
133
|
"""Install source packages to target directory."""
|
|
130
134
|
logger.info("install starting")
|
|
135
|
+
console = Console()
|
|
136
|
+
verbose = is_verbose_mode(ctx)
|
|
131
137
|
|
|
132
138
|
# Get target from options
|
|
133
139
|
target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
|
|
@@ -148,20 +154,64 @@ def install(
|
|
|
148
154
|
failures = extract_plan(plan, {Action.FAIL})
|
|
149
155
|
if failures:
|
|
150
156
|
can_install = False
|
|
151
|
-
|
|
152
|
-
f"Error: can't install {source_package}
|
|
157
|
+
console.print(
|
|
158
|
+
f"[red]✗ Error: can't install {source_package.name} - would overwrite:[/red]"
|
|
153
159
|
)
|
|
154
160
|
for plan_node in failures:
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
console.print(f" {target_path / plan_node.relative_destination_path}")
|
|
162
|
+
console.print()
|
|
157
163
|
|
|
158
164
|
if can_install:
|
|
159
|
-
#
|
|
165
|
+
# Count total actions
|
|
166
|
+
total_actions = sum(
|
|
167
|
+
len(extract_plan(plan, {Action.LINK, Action.CREATE}))
|
|
168
|
+
for _, plan in plans
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Open database and execute all plans with progress
|
|
160
172
|
with InstallationDB() as db:
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
if verbose:
|
|
174
|
+
# Verbose: show each file
|
|
175
|
+
for source_package, plan in plans:
|
|
176
|
+
console.print(f"[cyan]Installing {source_package.name}...[/cyan]")
|
|
177
|
+
for node in extract_plan(plan, {Action.LINK, Action.CREATE}):
|
|
178
|
+
console.print(f" {node.relative_destination_path}")
|
|
179
|
+
execute_plan(source_package, target_path, plan, db)
|
|
180
|
+
else:
|
|
181
|
+
# Default: show progress bar
|
|
182
|
+
with Progress(
|
|
183
|
+
SpinnerColumn(),
|
|
184
|
+
TextColumn("[progress.description]{task.description}"),
|
|
185
|
+
BarColumn(),
|
|
186
|
+
TaskProgressColumn(),
|
|
187
|
+
console=console,
|
|
188
|
+
) as progress:
|
|
189
|
+
task = progress.add_task("Installing...", total=total_actions)
|
|
190
|
+
for source_package, plan in plans:
|
|
191
|
+
progress.update(task, description=f"Installing {source_package.name}...")
|
|
192
|
+
execute_plan(source_package, target_path, plan, db)
|
|
193
|
+
progress.advance(task, len(extract_plan(plan, {Action.LINK, Action.CREATE})))
|
|
194
|
+
|
|
195
|
+
# Show summary
|
|
196
|
+
total_files = sum(
|
|
197
|
+
len(extract_plan(plan, {Action.LINK}))
|
|
198
|
+
for _, plan in plans
|
|
199
|
+
)
|
|
200
|
+
total_dirs = sum(
|
|
201
|
+
len(extract_plan(plan, {Action.CREATE}))
|
|
202
|
+
for _, plan in plans
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
summary_parts = []
|
|
206
|
+
if total_files:
|
|
207
|
+
summary_parts.append(f"{total_files} file(s)")
|
|
208
|
+
if total_dirs:
|
|
209
|
+
summary_parts.append(f"{total_dirs} dir(s)")
|
|
210
|
+
|
|
211
|
+
summary = " and ".join(summary_parts) if summary_parts else "nothing"
|
|
212
|
+
console.print(f"\n[green]✓ Installed {summary} from {len(sources)} package(s)[/green]")
|
|
163
213
|
else:
|
|
164
|
-
|
|
214
|
+
console.print("[red]✗ Refusing to install - conflicts detected[/red]")
|
|
165
215
|
|
|
166
216
|
logger.info("install finished")
|
|
167
217
|
|
|
@@ -182,6 +232,8 @@ def uninstall(
|
|
|
182
232
|
):
|
|
183
233
|
"""Uninstall source packages from target directory."""
|
|
184
234
|
logger.info("uninstall starting")
|
|
235
|
+
console = Console()
|
|
236
|
+
verbose = is_verbose_mode(ctx)
|
|
185
237
|
|
|
186
238
|
# Get target from options
|
|
187
239
|
target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
|
|
@@ -197,10 +249,42 @@ def uninstall(
|
|
|
197
249
|
)
|
|
198
250
|
plans.append((source_package, plan))
|
|
199
251
|
|
|
200
|
-
#
|
|
252
|
+
# Count total actions
|
|
253
|
+
total_actions = sum(
|
|
254
|
+
len(extract_plan(plan, {Action.UNLINK}))
|
|
255
|
+
for _, plan in plans
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Open database and execute all plans with progress
|
|
201
259
|
with InstallationDB() as db:
|
|
202
|
-
|
|
203
|
-
|
|
260
|
+
if verbose:
|
|
261
|
+
# Verbose: show each file
|
|
262
|
+
for source_package, plan in plans:
|
|
263
|
+
console.print(f"[cyan]Uninstalling {source_package.name}...[/cyan]")
|
|
264
|
+
for node in extract_plan(plan, {Action.UNLINK}):
|
|
265
|
+
console.print(f" {node.relative_destination_path}")
|
|
266
|
+
execute_plan(source_package, target_path, plan, db)
|
|
267
|
+
else:
|
|
268
|
+
# Default: show progress bar
|
|
269
|
+
with Progress(
|
|
270
|
+
SpinnerColumn(),
|
|
271
|
+
TextColumn("[progress.description]{task.description}"),
|
|
272
|
+
BarColumn(),
|
|
273
|
+
TaskProgressColumn(),
|
|
274
|
+
console=console,
|
|
275
|
+
) as progress:
|
|
276
|
+
task = progress.add_task("Uninstalling...", total=total_actions)
|
|
277
|
+
for source_package, plan in plans:
|
|
278
|
+
progress.update(task, description=f"Uninstalling {source_package.name}...")
|
|
279
|
+
execute_plan(source_package, target_path, plan, db)
|
|
280
|
+
progress.advance(task, len(extract_plan(plan, {Action.UNLINK})))
|
|
281
|
+
|
|
282
|
+
# Show summary
|
|
283
|
+
total_removed = sum(
|
|
284
|
+
len(extract_plan(plan, {Action.UNLINK}))
|
|
285
|
+
for _, plan in plans
|
|
286
|
+
)
|
|
287
|
+
console.print(f"\n[green]✓ Removed {total_removed} symlink(s) from {len(sources)} package(s)[/green]")
|
|
204
288
|
|
|
205
289
|
logger.info("uninstall finished")
|
|
206
290
|
|
|
@@ -214,35 +298,39 @@ def list_installed(
|
|
|
214
298
|
):
|
|
215
299
|
"""List all installed packages."""
|
|
216
300
|
logger.info("list starting")
|
|
301
|
+
console = Console()
|
|
217
302
|
|
|
218
303
|
with InstallationDB() as db:
|
|
219
304
|
packages = db.get_all_packages()
|
|
220
305
|
|
|
221
306
|
if not packages:
|
|
222
|
-
|
|
307
|
+
console.print("[yellow]No packages installed.[/yellow]")
|
|
223
308
|
return
|
|
224
309
|
|
|
225
310
|
if as_commands:
|
|
226
|
-
# Output as dotx install commands
|
|
311
|
+
# Output as dotx install commands (plain text, no formatting)
|
|
227
312
|
for pkg in packages:
|
|
228
313
|
typer.echo(f"dotx install {pkg['package_name']}")
|
|
229
314
|
else:
|
|
230
|
-
# Output as table
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
315
|
+
# Output as rich table
|
|
316
|
+
table = Table(title="Installed Packages", show_header=True, header_style="bold cyan")
|
|
317
|
+
table.add_column("Package", style="cyan", no_wrap=True)
|
|
318
|
+
table.add_column("Files", justify="right", style="magenta")
|
|
319
|
+
table.add_column("Last Install", style="green")
|
|
320
|
+
|
|
235
321
|
for pkg in packages:
|
|
236
322
|
package_name = Path(pkg["package_name"]).name
|
|
237
|
-
file_count = pkg["file_count"]
|
|
323
|
+
file_count = str(pkg["file_count"])
|
|
238
324
|
latest = (
|
|
239
325
|
pkg["latest_install"][:19]
|
|
240
326
|
if pkg["latest_install"]
|
|
241
|
-
else "unknown"
|
|
327
|
+
else "[dim]unknown[/dim]"
|
|
242
328
|
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
329
|
+
table.add_row(package_name, file_count, latest)
|
|
330
|
+
|
|
331
|
+
console.print()
|
|
332
|
+
console.print(table)
|
|
333
|
+
console.print(f"\n[bold]Total: {len(packages)} package(s)[/bold]\n")
|
|
246
334
|
|
|
247
335
|
logger.info("list finished")
|
|
248
336
|
|
|
@@ -262,6 +350,7 @@ def verify(
|
|
|
262
350
|
):
|
|
263
351
|
"""Verify installations against filesystem."""
|
|
264
352
|
logger.info("verify starting")
|
|
353
|
+
console = Console()
|
|
265
354
|
|
|
266
355
|
with InstallationDB() as db:
|
|
267
356
|
if package:
|
|
@@ -275,24 +364,24 @@ def verify(
|
|
|
275
364
|
]
|
|
276
365
|
|
|
277
366
|
if not packages_to_verify:
|
|
278
|
-
|
|
367
|
+
console.print("[yellow]No packages to verify.[/yellow]")
|
|
279
368
|
return
|
|
280
369
|
|
|
281
370
|
total_issues = 0
|
|
282
371
|
for pkg in packages_to_verify:
|
|
283
372
|
issues = db.verify_installations(pkg)
|
|
284
373
|
if issues:
|
|
285
|
-
|
|
374
|
+
console.print(f"\n[bold cyan]{pkg.name}:[/bold cyan]")
|
|
286
375
|
for issue in issues:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
376
|
+
console.print(f" [red]✗[/red] {issue['target_path']}")
|
|
377
|
+
console.print(f" [dim]Issue: {issue['issue']}[/dim]")
|
|
378
|
+
console.print(f" [dim]Expected: {issue['link_type']}[/dim]")
|
|
290
379
|
total_issues += len(issues)
|
|
291
380
|
|
|
292
381
|
if total_issues == 0:
|
|
293
|
-
|
|
382
|
+
console.print("[green]✓ All installations verified successfully.[/green]")
|
|
294
383
|
else:
|
|
295
|
-
|
|
384
|
+
console.print(f"\n[yellow]⚠ Found {total_issues} issue(s).[/yellow]")
|
|
296
385
|
|
|
297
386
|
logger.info("verify finished")
|
|
298
387
|
|
|
@@ -312,29 +401,78 @@ def show(
|
|
|
312
401
|
):
|
|
313
402
|
"""Show detailed installation information for a package."""
|
|
314
403
|
logger.info("show starting")
|
|
404
|
+
console = Console()
|
|
315
405
|
|
|
316
406
|
with InstallationDB() as db:
|
|
317
407
|
installations = db.get_installations(package)
|
|
318
408
|
|
|
319
409
|
if not installations:
|
|
320
|
-
|
|
410
|
+
console.print(f"[yellow]No installations found for {package.name}[/yellow]")
|
|
321
411
|
return
|
|
322
412
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
413
|
+
# Create info panel
|
|
414
|
+
info = f"[bold cyan]Package:[/bold cyan] {package}\n"
|
|
415
|
+
info += f"[bold cyan]Installed files:[/bold cyan] {len(installations)}"
|
|
416
|
+
|
|
417
|
+
panel = Panel(info, title="Package Information", border_style="cyan")
|
|
418
|
+
console.print()
|
|
419
|
+
console.print(panel)
|
|
420
|
+
|
|
421
|
+
# Create table for installations
|
|
422
|
+
table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
423
|
+
table.add_column("Target Path", style="cyan", no_wrap=False, overflow="fold")
|
|
424
|
+
table.add_column("Type", style="yellow")
|
|
425
|
+
table.add_column("Installed At", style="green")
|
|
327
426
|
|
|
328
427
|
for install in installations:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
428
|
+
table.add_row(
|
|
429
|
+
str(install['target_path']),
|
|
430
|
+
install['link_type'],
|
|
431
|
+
install['installed_at'][:19] if install['installed_at'] else "unknown"
|
|
432
|
+
)
|
|
332
433
|
|
|
333
|
-
|
|
434
|
+
console.print()
|
|
435
|
+
console.print(table)
|
|
436
|
+
console.print()
|
|
334
437
|
|
|
335
438
|
logger.info("show finished")
|
|
336
439
|
|
|
337
440
|
|
|
441
|
+
def _scan_symlinks(path: Path, max_depth: int, progress: Progress, task_id) -> list[tuple[Path, bool]]:
|
|
442
|
+
"""
|
|
443
|
+
Scan a directory for symlinks up to a maximum depth.
|
|
444
|
+
|
|
445
|
+
Returns list of (symlink_path, is_dir) tuples.
|
|
446
|
+
"""
|
|
447
|
+
symlinks = []
|
|
448
|
+
|
|
449
|
+
def _walk_limited(directory: Path, current_depth: int):
|
|
450
|
+
"""Recursively walk directory up to max_depth."""
|
|
451
|
+
if current_depth > max_depth:
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
for item in directory.iterdir():
|
|
456
|
+
# Update progress
|
|
457
|
+
progress.update(task_id, advance=1)
|
|
458
|
+
|
|
459
|
+
if item.is_symlink():
|
|
460
|
+
is_dir = item.is_dir() # Check if symlink points to directory
|
|
461
|
+
symlinks.append((item, is_dir))
|
|
462
|
+
# Don't descend into symlinked directories
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
# Recurse into regular directories
|
|
466
|
+
if item.is_dir() and not item.is_symlink():
|
|
467
|
+
_walk_limited(item, current_depth + 1)
|
|
468
|
+
|
|
469
|
+
except (PermissionError, OSError) as e:
|
|
470
|
+
logger.debug(f"Skipping {directory}: {e}")
|
|
471
|
+
|
|
472
|
+
_walk_limited(path, 0)
|
|
473
|
+
return symlinks
|
|
474
|
+
|
|
475
|
+
|
|
338
476
|
@app.command()
|
|
339
477
|
def sync(
|
|
340
478
|
ctx: click.Context,
|
|
@@ -342,37 +480,77 @@ def sync(
|
|
|
342
480
|
bool,
|
|
343
481
|
typer.Option("--dry-run", help="Show what would be added without modifying the database"),
|
|
344
482
|
] = False,
|
|
483
|
+
max_depth: Annotated[
|
|
484
|
+
Optional[int],
|
|
485
|
+
typer.Option(help="Maximum depth to scan (default: 1 for home, 3 for config)"),
|
|
486
|
+
] = None,
|
|
487
|
+
scan_paths: Annotated[
|
|
488
|
+
Optional[list[Path]],
|
|
489
|
+
typer.Option(help="Additional paths to scan"),
|
|
490
|
+
] = None,
|
|
491
|
+
simple: Annotated[
|
|
492
|
+
bool,
|
|
493
|
+
typer.Option(help="Simple scan: only home directory depth 1, skip ~/.config"),
|
|
494
|
+
] = False,
|
|
345
495
|
):
|
|
346
|
-
"""
|
|
496
|
+
"""
|
|
497
|
+
Rebuild database from filesystem (scan for existing symlinks).
|
|
498
|
+
|
|
499
|
+
By default, scans:
|
|
500
|
+
- Top-level of home directory (depth=1)
|
|
501
|
+
- All of ~/.config (depth=3)
|
|
502
|
+
|
|
503
|
+
Use --simple to only scan home directory at depth 1.
|
|
504
|
+
Use --max-depth to override default depth for all paths.
|
|
505
|
+
Use --scan-paths to add additional directories to scan.
|
|
506
|
+
"""
|
|
347
507
|
logger.info("sync starting")
|
|
508
|
+
console = Console()
|
|
348
509
|
|
|
349
510
|
# Get target from options
|
|
350
511
|
target_path = Path(ctx.obj.get("TARGET", Path.home())) if ctx.obj else Path.home()
|
|
351
512
|
|
|
352
|
-
#
|
|
513
|
+
# Determine scan strategy
|
|
514
|
+
scan_configs = []
|
|
515
|
+
|
|
516
|
+
if simple:
|
|
517
|
+
# Simple mode: just top-level of home
|
|
518
|
+
scan_configs.append((target_path, max_depth or 1))
|
|
519
|
+
else:
|
|
520
|
+
# Smart mode: top-level home + full ~/.config
|
|
521
|
+
scan_configs.append((target_path, max_depth or 1))
|
|
522
|
+
config_path = target_path / ".config"
|
|
523
|
+
if config_path.exists() and config_path.is_dir():
|
|
524
|
+
scan_configs.append((config_path, max_depth or 3))
|
|
525
|
+
|
|
526
|
+
# Add any user-specified paths
|
|
527
|
+
if scan_paths:
|
|
528
|
+
for path in scan_paths:
|
|
529
|
+
scan_configs.append((path, max_depth or 3))
|
|
530
|
+
|
|
531
|
+
# Scan filesystem for symlinks with progress
|
|
353
532
|
symlinks = []
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
533
|
+
|
|
534
|
+
with Progress(
|
|
535
|
+
SpinnerColumn(),
|
|
536
|
+
TextColumn("[progress.description]{task.description}"),
|
|
537
|
+
BarColumn(),
|
|
538
|
+
TaskProgressColumn(),
|
|
539
|
+
console=console,
|
|
540
|
+
) as progress:
|
|
541
|
+
scan_task = progress.add_task("Scanning for symlinks...", total=None)
|
|
542
|
+
|
|
543
|
+
for scan_path, depth in scan_configs:
|
|
544
|
+
progress.update(scan_task, description=f"Scanning {scan_path.name}...")
|
|
545
|
+
symlinks.extend(_scan_symlinks(scan_path, depth, progress, scan_task))
|
|
546
|
+
|
|
547
|
+
console.print(f"[green]✓[/green] Found {len(symlinks)} symlink(s)")
|
|
368
548
|
|
|
369
549
|
if not symlinks:
|
|
370
|
-
|
|
550
|
+
console.print("[yellow]No symlinks found.[/yellow]")
|
|
371
551
|
logger.info("sync finished - no symlinks found")
|
|
372
552
|
return
|
|
373
553
|
|
|
374
|
-
typer.echo(f"Found {len(symlinks)} symlink(s) in {target_path}")
|
|
375
|
-
|
|
376
554
|
# Group symlinks by their resolved source parent directory (package)
|
|
377
555
|
packages = {}
|
|
378
556
|
unknown = []
|
|
@@ -397,23 +575,23 @@ def sync(
|
|
|
397
575
|
unknown.append((link_path, None, is_dir))
|
|
398
576
|
|
|
399
577
|
# Show what was found
|
|
400
|
-
|
|
578
|
+
console.print(f"\n[bold]Discovered {len(packages)} potential package(s):[/bold]")
|
|
401
579
|
for package_root, links in packages.items():
|
|
402
|
-
|
|
403
|
-
|
|
580
|
+
console.print(f" [cyan]{package_root}[/cyan]")
|
|
581
|
+
console.print(f" {len(links)} symlink(s)")
|
|
404
582
|
|
|
405
583
|
if unknown:
|
|
406
|
-
|
|
584
|
+
console.print(f" [yellow]Unknown/broken: {len(unknown)} symlink(s)[/yellow]")
|
|
407
585
|
|
|
408
586
|
if dry_run:
|
|
409
|
-
|
|
587
|
+
console.print("\n[yellow]Dry run - no database changes made.[/yellow]")
|
|
410
588
|
logger.info("sync finished - dry run")
|
|
411
589
|
return
|
|
412
590
|
|
|
413
591
|
# Ask for confirmation
|
|
414
|
-
|
|
592
|
+
console.print("\n[bold]This will rebuild the database with the discovered installations.[/bold]")
|
|
415
593
|
if not typer.confirm("Continue?"):
|
|
416
|
-
|
|
594
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
417
595
|
logger.info("sync finished - cancelled by user")
|
|
418
596
|
return
|
|
419
597
|
|
|
@@ -434,7 +612,7 @@ def sync(
|
|
|
434
612
|
total_recorded += 1
|
|
435
613
|
logger.debug(f"Recorded {link_path} -> {package_root}")
|
|
436
614
|
|
|
437
|
-
|
|
615
|
+
console.print(f"\n[green]✓ Recorded {total_recorded} installation(s) in database.[/green]")
|
|
438
616
|
|
|
439
617
|
logger.info("sync finished")
|
|
440
618
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotx
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A command-line tool to install a link-farm to your dotfiles
|
|
5
5
|
Author-email: Wolf <Wolf@zv.cx>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: click>=8.1.7
|
|
23
23
|
Requires-Dist: loguru>=0.7.0
|
|
24
24
|
Requires-Dist: pathspec>=0.12.1
|
|
25
|
+
Requires-Dist: rich>=13.9.4
|
|
25
26
|
Requires-Dist: typer>=0.15.1
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
@@ -430,8 +430,9 @@ def test_cli_show_package(tmp_path, monkeypatch):
|
|
|
430
430
|
assert result.exit_code == 0
|
|
431
431
|
assert "Package:" in result.output
|
|
432
432
|
assert "Installed files: 2" in result.output
|
|
433
|
-
|
|
434
|
-
assert
|
|
433
|
+
# Rich may wrap long paths, so just check for the filenames
|
|
434
|
+
assert "file1" in result.output
|
|
435
|
+
assert "file2" in result.output
|
|
435
436
|
|
|
436
437
|
|
|
437
438
|
def test_cli_show_nonexistent_package(tmp_path, monkeypatch):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|