Skip to content

Commit 052baea

Browse files
authored
Merge pull request #18 from NK2552003/fix/features
add new changes and new features
2 parents 8e775f4 + 924adbe commit 052baea

16 files changed

Lines changed: 1513 additions & 1238 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
All notable changes to **mac-deep-cleaner** will be documented in this file.
44

5+
## v2.2.0 (2026-06-06)
6+
7+
### Added
8+
- Docker Cleanup scanner via `mdc clean --docker` to reclaim space from dangling images and unused volumes.
9+
- Interactive Terminal User Interface (TUI) via `mdc tui` (requires `textual`).
10+
- Deep App Uninstaller via `mdc uninstall` to simultaneously hunt and remove app bundles and their dependencies.
11+
- Background Scheduling enhancement via `mdc schedule install --clean` to automatically clean junk weekly.
12+
- DNS Cache Flushing via `mdc flush-dns` to troubleshoot networking routing issues.
13+
- `--auto-with-cache` flag to `clean` command to prompt for clearing System Caches.
14+
- Beautified CLI UI with rounded boxes, rich colors, and emojis for `reporter.py` and `cli.py`.
15+
- Dozens of modern apps (AI tools, dev environments, productivity apps) added to aliases and safe lists to improve detection.
16+
17+
### Fixed
18+
- Fixed bug where Brave Browser configurations (`BraveSoftware`) were incorrectly flagged as orphaned leftovers.
19+
- Fixed UI bug where an empty table was rendered for the "General Junk" category when no user-actionable junk was found.
20+
21+
### Changed
22+
- Refactored monolithic `constants.py` into a modular `src/constants/` package for better maintainability.
23+
524
## v2.0.1 (2026-05-22)
625

726
### Added

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "mac-deep-cleaner"
7-
version = "2.0.1"
7+
version = "2.2.0"
88
description = "Professional Mac cleanup tool — Smart App Orphan Detector"
99
readme = "README.md"
1010
license = "Apache-2.0"
@@ -15,6 +15,7 @@ dependencies = [
1515
"click>=8.1.0",
1616
"pyyaml>=6.0",
1717
"packaging>=23.0",
18+
"textual>=0.40.0",
1819
]
1920
classifiers = [
2021
"Development Status :: 4 - Beta",

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
Smart App Orphan Detector & System Cleanup Tool for macOS
33
"""
44

5-
__version__ = "2.0.1"
5+
__version__ = "2.2.0"
66
__author__ = "NK2552003"

src/cli.py

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def main(
182182
log_file: Optional[str],
183183
dry_run: bool,
184184
) -> None:
185-
"""Mac Deep Cleaner v2.0.1 — Professional macOS cleanup tool."""
185+
"""Mac Deep Cleaner v2.2.0 — Professional macOS cleanup tool."""
186186
from core.dry_run import set_dry_run
187187
configure_logging(
188188
verbose=verbose,
@@ -478,6 +478,10 @@ def render() -> Layout:
478478
@click.option("--notify", is_flag=True, default=False)
479479
@click.option("--no-undo", is_flag=True, default=False,
480480
help="Permanently delete instead of staging for undo.")
481+
@click.option("--docker", is_flag=True, default=False,
482+
help="Clean unused Docker containers, images, and networks.")
483+
@click.option("--auto-with-cache", is_flag=True, default=False,
484+
help="Prompt to delete system caches when using auto clean.")
481485
@click.pass_context
482486
def clean(
483487
ctx: click.Context,
@@ -492,6 +496,8 @@ def clean(
492496
custom_roots: Tuple[str, ...],
493497
notify: bool,
494498
no_undo: bool,
499+
auto_with_cache: bool,
500+
docker: bool,
495501
) -> None:
496502
"""Interactively clean orphaned app leftovers and junk.
497503
@@ -513,6 +519,14 @@ def clean(
513519
if dry_run:
514520
console.print("[yellow]Dry-run enabled; clean will run in preview mode.[/yellow]")
515521

522+
clean_system_caches = False
523+
if auto_with_cache and not dry_run:
524+
from rich.prompt import Confirm
525+
clean_system_caches = Confirm.ask(
526+
"[bold red]Do you want to delete ~/Library/Caches as well? (System caches)[/bold red]",
527+
default=False
528+
)
529+
516530
_run(
517531
delete=not dry_run,
518532
auto=auto,
@@ -527,9 +541,77 @@ def clean(
527541
undo_mode=undo_mode,
528542
dev_junk=dev_junk,
529543
dev_junk_global=dev_junk_global,
544+
clean_system_caches=clean_system_caches,
545+
docker=docker,
530546
)
531547

532548

549+
# ══════════════════════════════════════════════════════════════════════════════
550+
# ══════════════════════════════════════════════════════════════════════════════
551+
# UNINSTALL & DNS FLUSH
552+
# ══════════════════════════════════════════════════════════════════════════════
553+
554+
@main.command()
555+
@click.argument("app_name")
556+
@click.option("--auto", is_flag=True, default=False, help="Skip confirmation prompt")
557+
@click.option("--no-undo", is_flag=True, default=False, help="Permanently delete instead of staging for undo")
558+
@click.pass_context
559+
def uninstall(ctx: click.Context, app_name: str, auto: bool, no_undo: bool) -> None:
560+
"""Deep uninstall an application and all its data."""
561+
from core.uninstaller import find_app_candidates, build_uninstall_plan, execute_uninstall
562+
from core.scanner import discover_installed_apps
563+
from core.undo import new_session
564+
from rich.prompt import Confirm
565+
from utils import bytes_human
566+
567+
apps = discover_installed_apps()
568+
candidates = find_app_candidates(app_name, apps)
569+
if not candidates:
570+
console.print(f"[red]Could not find an installed app matching '{app_name}'.[/red]")
571+
return
572+
573+
app = candidates[0]
574+
if len(candidates) > 1:
575+
console.print(f"[yellow]Multiple apps matched. Selecting {app.name} ({app.bundle_id}).[/yellow]")
576+
577+
plan = build_uninstall_plan(app)
578+
579+
console.print(f"\n[bold red]Uninstall Plan for {app.name}[/bold red]")
580+
console.print(f"Total size: {bytes_human(plan.total_size)}")
581+
for item in plan.deletable_items:
582+
console.print(f" [dim]- {item.path}[/dim] ([yellow]{bytes_human(item.size)}[/yellow])")
583+
584+
do_it = auto or Confirm.ask(f"\nUninstall {app.name} and delete {bytes_human(plan.total_size)}?", default=False)
585+
if do_it:
586+
cfg = load_config()
587+
session = new_session() if (cfg.undo_mode and not no_undo) else None
588+
res = execute_uninstall(plan, session=session)
589+
console.print(f"\n[green]✓ Freed {bytes_human(res.bytes_freed)}. Removed {res.deleted} items. Staged {res.staged}.[/green]")
590+
591+
@main.command("flush-dns")
592+
def flush_dns() -> None:
593+
"""Flush DNS and network caches."""
594+
from core.dns_cache import flush_dns_cache
595+
596+
console.print("Flushing DNS caches...")
597+
res = flush_dns_cache()
598+
if res.success:
599+
console.print("[bold green]✓ DNS cache flushed successfully.[/bold green]")
600+
else:
601+
console.print("[bold red]✗ Failed to flush DNS cache.[/bold red]")
602+
603+
@main.command("tui")
604+
def tui() -> None:
605+
"""Launch the interactive Terminal User Interface."""
606+
try:
607+
import textual
608+
except ImportError:
609+
console.print("[red]Textual is not installed. Run 'poetry install' or install textual manually.[/red]")
610+
return
611+
612+
from tui.app import run_tui
613+
run_tui()
614+
533615
# ══════════════════════════════════════════════════════════════════════════════
534616
# DEVELOPER JUNK
535617
# ══════════════════════════════════════════════════════════════════════════════
@@ -3622,14 +3704,15 @@ def cmd_schedule() -> None:
36223704

36233705
@cmd_schedule.command("install")
36243706
@click.option("--no-notify", is_flag=True, default=False)
3707+
@click.option("--clean", is_flag=True, default=False, help="Run background clean instead of just scan.")
36253708
@click.pass_context
3626-
def schedule_install(ctx: click.Context, no_notify: bool) -> None:
3709+
def schedule_install(ctx: click.Context, no_notify: bool, clean: bool) -> None:
36273710
"""Install a weekly LaunchAgent to run scans automatically."""
36283711
from core.dry_run import skip_if_dry_run
36293712
if skip_if_dry_run(ctx, console, "schedule install"):
36303713
return
36313714
from core.scheduler import install_schedule
3632-
ok, msg = install_schedule(notify=not no_notify)
3715+
ok, msg = install_schedule(notify=not no_notify, clean=clean)
36333716
color = "green" if ok else "red"
36343717
console.print(f"\n [{color}]{msg}[/{color}]\n")
36353718

@@ -3779,6 +3862,8 @@ def _run(
37793862
dev_junk_global: bool = False,
37803863
ci: bool = False,
37813864
threshold_mb: int = 0,
3865+
clean_system_caches: bool = False,
3866+
docker: bool = False,
37823867
) -> None:
37833868
"""Core scan + optional cleanup logic (shared by scan and clean)."""
37843869
# Local import to satisfy static analysis and avoid name-resolution issues.
@@ -3984,10 +4069,31 @@ def _run_step(description: str, fn):
39844069
except Exception as exc:
39854070
logger.debug("Failed to send notification: %s", exc)
39864071

3987-
if grand == 0:
4072+
if grand == 0 and not docker:
39884073
console.print("\n[bold green]✓ Your Mac is spotless! Nothing to clean.[/bold green]\n")
39894074
return
39904075

4076+
if docker:
4077+
from scanners.docker_cleaner import scan_docker_bloat, clean_docker_bloat
4078+
bloat = scan_docker_bloat()
4079+
if bloat:
4080+
console.print()
4081+
console.rule("[bold]Docker Junk", style="blue")
4082+
docker_total = sum(e.size for e in bloat)
4083+
console.print(f" [blue]Docker Bloat:[/blue] {bytes_human(docker_total)}")
4084+
4085+
do_del = auto
4086+
if not do_del:
4087+
from rich.prompt import Confirm
4088+
do_del = Confirm.ask(
4089+
f" Delete Docker bloat ([yellow]{bytes_human(docker_total)}[/yellow])?",
4090+
default=False,
4091+
)
4092+
if do_del:
4093+
freed_docker = clean_docker_bloat()
4094+
console.print(f" [green]✓ Freed {bytes_human(freed_docker)} from Docker[/green]")
4095+
grand += freed_docker
4096+
39914097
# Diff hint
39924098
try:
39934099
from config.history import list_history, diff_scans
@@ -4035,7 +4141,11 @@ def _run_step(description: str, fn):
40354141
if ok:
40364142
freed += sz
40374143

4038-
user_junk = [j for j in junk if not j.is_system]
4144+
if clean_system_caches:
4145+
user_junk = junk
4146+
else:
4147+
user_junk = [j for j in junk if not j.is_system]
4148+
40394149
if user_junk:
40404150
do_del = auto
40414151
if not do_del:
@@ -4087,7 +4197,7 @@ def _run_step(description: str, fn):
40874197
f"[dim]Restore with: mac-cleaner undo --session {session.session_id[:8]}[/dim]"
40884198
)
40894199
else:
4090-
freed = do_cleanup(orphans, junk, auto=auto)
4200+
freed = do_cleanup(orphans, junk, auto=auto, clean_system_caches=clean_system_caches)
40914201
if dev_junk_entries:
40924202
from rich.prompt import Confirm
40934203
do_del = auto or Confirm.ask(

0 commit comments

Comments
 (0)