From 924adbec4cfcda1ca90357a49fa1c972ed9ddaab Mon Sep 17 00:00:00 2001 From: Nitish Kumar Date: Sat, 6 Jun 2026 09:10:17 +0530 Subject: [PATCH] add new changes and new features --- CHANGELOG.md | 19 + pyproject.toml | 3 +- src/__init__.py | 2 +- src/cli.py | 122 +++- src/constants.py | 1189 -------------------------------- src/constants/__init__.py | 54 ++ src/constants/aliases.py | 339 +++++++++ src/constants/paths.py | 84 +++ src/constants/system.py | 531 ++++++++++++++ src/constants/team_ids.py | 102 +++ src/constants/vendors.py | 34 + src/core/cleaner.py | 6 +- src/core/scheduler.py | 15 +- src/reporting/reporter.py | 82 ++- src/scanners/docker_cleaner.py | 106 +++ src/tui/app.py | 63 ++ 16 files changed, 1513 insertions(+), 1238 deletions(-) delete mode 100644 src/constants.py create mode 100644 src/constants/__init__.py create mode 100644 src/constants/aliases.py create mode 100644 src/constants/paths.py create mode 100644 src/constants/system.py create mode 100644 src/constants/team_ids.py create mode 100644 src/constants/vendors.py create mode 100644 src/scanners/docker_cleaner.py create mode 100644 src/tui/app.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2c28d..ecede12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to **mac-deep-cleaner** will be documented in this file. +## v2.2.0 (2026-06-06) + +### Added +- Docker Cleanup scanner via `mdc clean --docker` to reclaim space from dangling images and unused volumes. +- Interactive Terminal User Interface (TUI) via `mdc tui` (requires `textual`). +- Deep App Uninstaller via `mdc uninstall` to simultaneously hunt and remove app bundles and their dependencies. +- Background Scheduling enhancement via `mdc schedule install --clean` to automatically clean junk weekly. +- DNS Cache Flushing via `mdc flush-dns` to troubleshoot networking routing issues. +- `--auto-with-cache` flag to `clean` command to prompt for clearing System Caches. +- Beautified CLI UI with rounded boxes, rich colors, and emojis for `reporter.py` and `cli.py`. +- Dozens of modern apps (AI tools, dev environments, productivity apps) added to aliases and safe lists to improve detection. + +### Fixed +- Fixed bug where Brave Browser configurations (`BraveSoftware`) were incorrectly flagged as orphaned leftovers. +- Fixed UI bug where an empty table was rendered for the "General Junk" category when no user-actionable junk was found. + +### Changed +- Refactored monolithic `constants.py` into a modular `src/constants/` package for better maintainability. + ## v2.0.1 (2026-05-22) ### Added diff --git a/pyproject.toml b/pyproject.toml index 642915e..b55bfd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mac-deep-cleaner" -version = "2.0.1" +version = "2.2.0" description = "Professional Mac cleanup tool — Smart App Orphan Detector" readme = "README.md" license = "Apache-2.0" @@ -15,6 +15,7 @@ dependencies = [ "click>=8.1.0", "pyyaml>=6.0", "packaging>=23.0", + "textual>=0.40.0", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/src/__init__.py b/src/__init__.py index 7dbcf0c..34bb9f8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,5 +2,5 @@ Smart App Orphan Detector & System Cleanup Tool for macOS """ -__version__ = "2.0.1" +__version__ = "2.2.0" __author__ = "NK2552003" diff --git a/src/cli.py b/src/cli.py index fa4d1a4..398837b 100644 --- a/src/cli.py +++ b/src/cli.py @@ -182,7 +182,7 @@ def main( log_file: Optional[str], dry_run: bool, ) -> None: - """Mac Deep Cleaner v2.0.1 — Professional macOS cleanup tool.""" + """Mac Deep Cleaner v2.2.0 — Professional macOS cleanup tool.""" from core.dry_run import set_dry_run configure_logging( verbose=verbose, @@ -478,6 +478,10 @@ def render() -> Layout: @click.option("--notify", is_flag=True, default=False) @click.option("--no-undo", is_flag=True, default=False, help="Permanently delete instead of staging for undo.") +@click.option("--docker", is_flag=True, default=False, + help="Clean unused Docker containers, images, and networks.") +@click.option("--auto-with-cache", is_flag=True, default=False, + help="Prompt to delete system caches when using auto clean.") @click.pass_context def clean( ctx: click.Context, @@ -492,6 +496,8 @@ def clean( custom_roots: Tuple[str, ...], notify: bool, no_undo: bool, + auto_with_cache: bool, + docker: bool, ) -> None: """Interactively clean orphaned app leftovers and junk. @@ -513,6 +519,14 @@ def clean( if dry_run: console.print("[yellow]Dry-run enabled; clean will run in preview mode.[/yellow]") + clean_system_caches = False + if auto_with_cache and not dry_run: + from rich.prompt import Confirm + clean_system_caches = Confirm.ask( + "[bold red]Do you want to delete ~/Library/Caches as well? (System caches)[/bold red]", + default=False + ) + _run( delete=not dry_run, auto=auto, @@ -527,9 +541,77 @@ def clean( undo_mode=undo_mode, dev_junk=dev_junk, dev_junk_global=dev_junk_global, + clean_system_caches=clean_system_caches, + docker=docker, ) +# ══════════════════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════════════ +# UNINSTALL & DNS FLUSH +# ══════════════════════════════════════════════════════════════════════════════ + +@main.command() +@click.argument("app_name") +@click.option("--auto", is_flag=True, default=False, help="Skip confirmation prompt") +@click.option("--no-undo", is_flag=True, default=False, help="Permanently delete instead of staging for undo") +@click.pass_context +def uninstall(ctx: click.Context, app_name: str, auto: bool, no_undo: bool) -> None: + """Deep uninstall an application and all its data.""" + from core.uninstaller import find_app_candidates, build_uninstall_plan, execute_uninstall + from core.scanner import discover_installed_apps + from core.undo import new_session + from rich.prompt import Confirm + from utils import bytes_human + + apps = discover_installed_apps() + candidates = find_app_candidates(app_name, apps) + if not candidates: + console.print(f"[red]Could not find an installed app matching '{app_name}'.[/red]") + return + + app = candidates[0] + if len(candidates) > 1: + console.print(f"[yellow]Multiple apps matched. Selecting {app.name} ({app.bundle_id}).[/yellow]") + + plan = build_uninstall_plan(app) + + console.print(f"\n[bold red]Uninstall Plan for {app.name}[/bold red]") + console.print(f"Total size: {bytes_human(plan.total_size)}") + for item in plan.deletable_items: + console.print(f" [dim]- {item.path}[/dim] ([yellow]{bytes_human(item.size)}[/yellow])") + + do_it = auto or Confirm.ask(f"\nUninstall {app.name} and delete {bytes_human(plan.total_size)}?", default=False) + if do_it: + cfg = load_config() + session = new_session() if (cfg.undo_mode and not no_undo) else None + res = execute_uninstall(plan, session=session) + console.print(f"\n[green]✓ Freed {bytes_human(res.bytes_freed)}. Removed {res.deleted} items. Staged {res.staged}.[/green]") + +@main.command("flush-dns") +def flush_dns() -> None: + """Flush DNS and network caches.""" + from core.dns_cache import flush_dns_cache + + console.print("Flushing DNS caches...") + res = flush_dns_cache() + if res.success: + console.print("[bold green]✓ DNS cache flushed successfully.[/bold green]") + else: + console.print("[bold red]✗ Failed to flush DNS cache.[/bold red]") + +@main.command("tui") +def tui() -> None: + """Launch the interactive Terminal User Interface.""" + try: + import textual + except ImportError: + console.print("[red]Textual is not installed. Run 'poetry install' or install textual manually.[/red]") + return + + from tui.app import run_tui + run_tui() + # ══════════════════════════════════════════════════════════════════════════════ # DEVELOPER JUNK # ══════════════════════════════════════════════════════════════════════════════ @@ -3622,14 +3704,15 @@ def cmd_schedule() -> None: @cmd_schedule.command("install") @click.option("--no-notify", is_flag=True, default=False) +@click.option("--clean", is_flag=True, default=False, help="Run background clean instead of just scan.") @click.pass_context -def schedule_install(ctx: click.Context, no_notify: bool) -> None: +def schedule_install(ctx: click.Context, no_notify: bool, clean: bool) -> None: """Install a weekly LaunchAgent to run scans automatically.""" from core.dry_run import skip_if_dry_run if skip_if_dry_run(ctx, console, "schedule install"): return from core.scheduler import install_schedule - ok, msg = install_schedule(notify=not no_notify) + ok, msg = install_schedule(notify=not no_notify, clean=clean) color = "green" if ok else "red" console.print(f"\n [{color}]{msg}[/{color}]\n") @@ -3779,6 +3862,8 @@ def _run( dev_junk_global: bool = False, ci: bool = False, threshold_mb: int = 0, + clean_system_caches: bool = False, + docker: bool = False, ) -> None: """Core scan + optional cleanup logic (shared by scan and clean).""" # Local import to satisfy static analysis and avoid name-resolution issues. @@ -3984,10 +4069,31 @@ def _run_step(description: str, fn): except Exception as exc: logger.debug("Failed to send notification: %s", exc) - if grand == 0: + if grand == 0 and not docker: console.print("\n[bold green]✓ Your Mac is spotless! Nothing to clean.[/bold green]\n") return + if docker: + from scanners.docker_cleaner import scan_docker_bloat, clean_docker_bloat + bloat = scan_docker_bloat() + if bloat: + console.print() + console.rule("[bold]Docker Junk", style="blue") + docker_total = sum(e.size for e in bloat) + console.print(f" [blue]Docker Bloat:[/blue] {bytes_human(docker_total)}") + + do_del = auto + if not do_del: + from rich.prompt import Confirm + do_del = Confirm.ask( + f" Delete Docker bloat ([yellow]{bytes_human(docker_total)}[/yellow])?", + default=False, + ) + if do_del: + freed_docker = clean_docker_bloat() + console.print(f" [green]✓ Freed {bytes_human(freed_docker)} from Docker[/green]") + grand += freed_docker + # Diff hint try: from config.history import list_history, diff_scans @@ -4035,7 +4141,11 @@ def _run_step(description: str, fn): if ok: freed += sz - user_junk = [j for j in junk if not j.is_system] + if clean_system_caches: + user_junk = junk + else: + user_junk = [j for j in junk if not j.is_system] + if user_junk: do_del = auto if not do_del: @@ -4087,7 +4197,7 @@ def _run_step(description: str, fn): f"[dim]Restore with: mac-cleaner undo --session {session.session_id[:8]}[/dim]" ) else: - freed = do_cleanup(orphans, junk, auto=auto) + freed = do_cleanup(orphans, junk, auto=auto, clean_system_caches=clean_system_caches) if dev_junk_entries: from rich.prompt import Confirm do_del = auto or Confirm.ask( diff --git a/src/constants.py b/src/constants.py deleted file mode 100644 index 181496c..0000000 --- a/src/constants.py +++ /dev/null @@ -1,1189 +0,0 @@ - -"""Constants and configuration data for scans and safety rules.""" - -from pathlib import Path -from typing import Dict, List, Set - -HOME = Path.home() -LOG_FILE = HOME / ".mac_cleaner_deleted.log" -CONFIG_DIR = HOME / ".config" / "mac-cleaner" - -# ── Scan roots ──────────────────────────────────────────────────────────────── - -SEARCH_ROOTS: List[Path] = [ - HOME / "Library" / "Application Support", - HOME / "Library" / "Preferences", - HOME / "Library" / "Caches", - HOME / "Library" / "Logs", - HOME / "Library" / "LaunchAgents", - HOME / "Library" / "Containers", - HOME / "Library" / "Group Containers", - HOME / "Library" / "Saved Application State", - HOME / "Library" / "WebKit", - HOME / "Library" / "HTTPStorages", - HOME / "Library" / "Cookies", - HOME / "Library" / "SyncedPreferences", - Path("/Library/Application Support"), - Path("/Library/Preferences"), - Path("/Library/Caches"), - Path("/Library/LaunchAgents"), - Path("/Library/LaunchDaemons"), - Path("/Library/Logs"), - Path("/Library/PrivilegedHelperTools"), -] - -# ── App Discovery Directories ───────────────────────────────────────────────── - -APP_SEARCH_DIRS: List[Path] = [ - Path("/Applications"), - HOME / "Applications", - Path("/System/Applications"), - Path("/System/Library/CoreServices"), - Path("/Applications/Utilities"), - Path("/System/Applications/Utilities"), - Path("/System/Library/PreferencePanes"), - HOME / "Library" / "PreferencePanes", -] - -# ── App data directory aliases ──────────────────────────────────────────────── -# Maps lowercased folder names to bundle ID prefixes. -# Prevents false positives for apps using non-standard dir names. - -APP_DIR_ALIASES: Dict[str, str] = { - # ── Microsoft ───────────────────────────────────────────────────────── - "code": "com.microsoft.vscode", - "code - insiders": "com.microsoft.vscode-insiders", - "microsoft edge": "com.microsoft.edgemac", - "microsoft autoupdate": "com.microsoft.autoupdate2", - "onedrive": "com.microsoft.onedrive", - "microsoft onenote": "com.microsoft.onenote.mac", - "microsoft outlook": "com.microsoft.outlook", - "microsoft word": "com.microsoft.word", - "microsoft excel": "com.microsoft.excel", - "microsoft powerpoint": "com.microsoft.powerpoint", - "microsoft teams": "com.microsoft.teams", - "microsoft teams (work or school)": "com.microsoft.teams2", - "microsoft teams classic": "com.microsoft.teams", - # short-form aliases for Microsoft apps - "excel": "com.microsoft.excel", - "word": "com.microsoft.word", - "powerpoint": "com.microsoft.powerpoint", - "outlook": "com.microsoft.outlook", - "onenote": "com.microsoft.onenote.mac", - "teams": "com.microsoft.teams", - "copilot": "com.microsoft.copilot", - "visual studio": "com.microsoft.visual-studio", - "visual studio code": "com.microsoft.vscode", - - # ── Google ──────────────────────────────────────────────────────────── - "google": "com.google", - "chrome": "com.google.chrome", - "google chrome": "com.google.chrome", - "google chrome canary": "com.google.chrome.canary", - "google drive": "com.google.drivefs", - "google earth pro": "com.google.googleearthpro", - # v2.0.0 - "googledrive": "com.google.drivefs", - - # ── JetBrains ───────────────────────────────────────────────────────── - "jetbrains": "com.jetbrains", - "intellij idea": "com.jetbrains.intellij", - "intellij idea ce": "com.jetbrains.intellij.ce", - "pycharm": "com.jetbrains.pycharm", - "pycharm ce": "com.jetbrains.pycharm.ce", - "webstorm": "com.jetbrains.webstorm", - "goland": "com.jetbrains.goland", - "clion": "com.jetbrains.clion", - "datagrip": "com.jetbrains.datagrip", - "rider": "com.jetbrains.rider", - "rubymine": "com.jetbrains.rubymine", - "phpstorm": "com.jetbrains.phpstorm", - "appcode": "com.jetbrains.appcode", - "fleet": "com.jetbrains.fleet", - "jetbrains toolbox": "com.jetbrains.toolbox", - "jetbrains toolbox app": "com.jetbrains.toolbox", - - # ── Browsers ────────────────────────────────────────────────────────── - "firefox": "org.mozilla.firefox", - "mozilla firefox": "org.mozilla.firefox", - "firefox nightly": "org.mozilla.nightly", - "chromium": "org.chromium.chromium", - "brave browser": "com.brave.browser", - "brave browser beta": "com.brave.browser.beta", - "opera": "com.operasoftware.opera", - "opera gx": "com.operasoftware.operagx", - "vivaldi": "com.vivaldi.vivaldi", - "arc": "company.thebrowser.browser", - "orion": "com.nickvision.orion", - "tor browser": "org.torproject.torbrowser", - "waterfox": "net.nickolaj.nickelodeon", - "sidekick": "com.nicklodeon.nickelodeon", - # extra short-form browser aliases - "brave": "com.brave.browser", - "edge": "com.microsoft.edgemac", - - # ── Communication ───────────────────────────────────────────────────── - "slack": "com.tinyspeck.slackmacgap", - "discord": "com.hnc.discord", - "telegram": "ru.keepcoder.telegram", - "telegram desktop": "ru.keepcoder.telegram", - "whatsapp": "net.whatsapp.whatsapp", - "signal": "org.whispersystems.signal-desktop", - "zoom": "us.zoom.xos", - "zoom.us": "us.zoom.xos", - "skype": "com.skype.skype", - "zulip": "org.zulip.zulip", - "mattermost": "com.mattermost.desktop", - "element": "io.element.desktop", - "thunderbird": "org.mozilla.thunderbird", - "spark": "com.readdle.smartemail.macos", - "webex": "com.cisco.webexmeetingsapp", - - # ── Productivity ────────────────────────────────────────────────────── - "notion": "notion.id", - "notion enhanced": "notion.id", - "obsidian": "md.obsidian", - "logseq": "com.electron.logseq", - "bear": "net.shinyfrog.bear", - "craft": "io.craft.desktop", - "1password 7": "com.agilebits.onepassword7", - "1password": "com.1password.1password", - "lastpass": "com.lastpass.lastpassmacdesktop", - "bitwarden": "com.bitwarden.desktop", - "alfred": "com.runningwithcrayons.alfred", - "raycast": "com.raycast.macos", - "todoist": "com.todoist.macos", - "things": "com.culturedcode.thingsmac", - "things 3": "com.culturedcode.thingsmac", - "fantastical": "com.flexibits.fantastical2.mac", - "fantastical 2": "com.flexibits.fantastical2.mac", - "cardhop": "com.flexibits.cardhop.mac", - "day one": "com.bloombuilt.dayone-mac", - "drafts": "com.agiletortoise.drafts", - "grammarly": "com.grammarly.projectllama", - "grammarly desktop": "com.grammarly.projectllama", - "evernote": "com.evernote.evernote", - "typora": "abnerworks.typora", - "ulysses": "com.soulmen.ulysses", - "ia writer": "pro.writer.mac", - "devonthink 3": "com.devon-technologies.think3", - "devonthink": "com.devon-technologies.think3", - "linear": "com.linear.linear", - "superhuman": "com.superhuman.desktop", - - # ── Design ──────────────────────────────────────────────────────────── - "figma": "com.figma.desktop", - "sketch": "com.bohemiancoding.sketch3", - "zeplin": "io.zeplin.zeplin", - "invision": "com.invisionapp.studio", - "pixelmator pro": "com.pixelmatorteam.pixelmator.x", - "affinity photo": "com.seriflabs.affinityphoto", - "affinity photo 2": "com.seriflabs.affinityphoto2", - "affinity designer": "com.seriflabs.affinitydesigner", - "affinity designer 2": "com.seriflabs.affinitydesigner2", - "affinity publisher": "com.seriflabs.affinitypublisher", - "affinity publisher 2": "com.seriflabs.affinitypublisher2", - "inkscape": "org.inkscape.inkscape", - "gimp": "org.gimp.gimp-2.10", - "canva": "com.canva.canva", - "principle": "com.principleformac.principle", - "pixelmator": "com.pixelmatorteam.pixelmator", - - # ── Dev tools ───────────────────────────────────────────────────────── - "iterm2": "com.googlecode.iterm2", - "iterm": "com.googlecode.iterm2", - "warp": "dev.warp.warp-stable", - "cursor": "com.todesktop.230313mzl4w4u92", - "nova": "com.panic.nova", - "bbedit": "com.barebones.bbedit", - "transmit 5": "com.panic.transmit", - "transmit": "com.panic.transmit", - "cyberduck": "ch.sudo.cyberduck", - "tableplus": "com.tinyapp.tableplus", - "sequel pro": "com.sequelpro.sequelpro", - "sequel ace": "com.sequel-ace.sequel-ace", - "proxyman": "com.proxyman.nsproxy", - "charles": "com.xk72.charles", - "paw": "com.luckymarmot.paw", - "rapidapi": "com.luckymarmot.paw", - "httpie": "io.httpie.app", - "postman": "com.postmanlabs.mac", - "insomnia": "com.insomnia.app", - "sublime text": "com.sublimetext.4", - "sublime merge": "com.sublimemerge", - "atom": "com.github.atom", - "xcodes": "com.robotsandpencils.xcodes", - "dash": "com.kapeli.dashmacextras", - "kaleidoscope": "com.blackpixel.kaleidoscope", - "coderunner": "com.krill.coderunner", - "coteditor": "com.coteditor.coteditor", - "textedit": "com.apple.textedit", - "simulator": "com.apple.iphonesimulator", - "xcode": "com.apple.dt.xcode", - "textmate": "com.macromates.textmate", - - # ── Source Control ──────────────────────────────────────────────────── - "tower": "com.fournova.tower3", - "sourcetree": "com.torusknot.sourcetreenotmas", - "fork": "com.danlec.fork", - "gitkraken": "com.axosoft.gitkraken", - "github desktop": "com.github.ghd", - "git-annex": "com.manton.gitannex", - - # ── Storage / Cloud ─────────────────────────────────────────────────── - "dropbox": "com.getdropbox.dropbox", - "boxsync": "com.box.desktop", - "box": "com.box.desktop", - "arq": "com.haystacksoftware.arq", - "backblaze": "com.backblaze.backblaze", - "syncthing": "com.syncthing.syncthing", - "resilio sync": "com.resilio.sync", - "tresorit": "com.tresorit.mac", - "mountain duck": "io.mountainduck", - "expandrive": "com.expandrive.expandrive3", - - # ── Media ───────────────────────────────────────────────────────────── - "spotify": "com.spotify.client", - "vlc": "org.videolan.vlc", - "iina": "com.colliderli.iina", - "handbrake": "fr.handbrake.handbrake", - "permute 3": "com.charliemonroe.permute", - "permute": "com.charliemonroe.permute", - "downie": "com.charliemonroe.downie", - "downie 4": "com.charliemonroe.downie-4", - "bezel": "com.wearereasonablepeople.bezel", - "davinci resolve": "com.blackmagic-design.davinciresolve", - "obs": "com.obsproject.obs-studio", - "obs studio": "com.obsproject.obs-studio", - "audacity": "org.audacityteam.audacity", - "screenflow": "net.telestream.screenflow9", - "screenflow 10": "net.telestream.screenflow10", - "mpv": "io.mpv.player", - "plex": "tv.plex.plexmediaplayer", - "infuse": "com.firecore.infuse", - - # ── System utilities ────────────────────────────────────────────────── - "bartender 4": "com.surteesstudios.bartender", - "bartender": "com.surteesstudios.bartender", - "bartender 5": "com.surteesstudios.bartender5", - "magnet": "com.crowdcafe.windowmagnet", - "rectangle": "com.knollsoft.rectangle", - "rectangle pro": "com.knollsoft.rectanglepro", - "macs fan control": "crystalidea.macs-fan-control", - "istatmenus": "com.bjango.istatmenus", - "istat menus": "com.bjango.istatmenus", - "cleanmymac x": "com.macpaw.cleanmymac4", - "cleanmymac": "com.macpaw.cleanmymac4", - "hazel": "com.noodlesoft.hazel", - "keyboard maestro": "com.stairways.keyboardmaestro", - "bettertouchtool": "com.hegenberg.bettertouchtool", - "karabiner-elements": "org.pqrs.karabiner-elements", - "karabiner": "org.pqrs.karabiner-elements", - "popclip": "com.pilotmoon.popclip", - "audio hijack": "com.rogueamoeba.audiohijack3", - "loopback": "com.rogueamoeba.loopback2", - "soundsource": "com.rogueamoeba.soundsource", - "appcleaner": "com.freemacsoft.appcleaner", - "the unarchiver": "com.macpaw.site.theunarchiver", - "keka": "com.aone.keka", - "coconutbattery": "com.coconut-flavour.coconutbattery3", - "aldente": "com.apphousekitchen.aldente-pro", - "amphetamine": "com.if.amphetamine", - "lungo": "com.sindresorhus.lungo", - "stats": "eu.exelban.stats", - "monitorcontrol": "me.guillaumeb.monitorcontrol", - "hiddenbar": "com.dwarvesv.minimalbar", - "dozer": "com.mortennn.dozer", - "yabai": "com.koekeishiya.yabai", - "skhd": "com.koekeishiya.skhd", - "alt-tab": "com.lwouis.alt-tab-macos", - "hammerspoon": "org.hammerspoon.hammerspoon", - "setapp": "com.macpaw.setapp", - "macupdater": "com.corecode.macupdater", - "sparkle": "org.sparkle-project.sparkle", - - # ── Virtualisation ──────────────────────────────────────────────────── - "virtualbox": "org.virtualbox.app.virtualbox", - "parallels": "com.parallels.desktop.console", - "parallels desktop": "com.parallels.desktop.console", - "vmware fusion": "com.vmware.fusion", - "docker": "com.docker.docker", - "docker desktop": "com.docker.docker", - "utm": "com.utmapp.utm", - "multipass": "com.canonical.multipass", - - # ── Gaming ──────────────────────────────────────────────────────────── - "steam": "com.valvesoftware.steam", - "gog galaxy": "com.gogcom.galaxyclient", - "epic games": "com.epicgames.launcher", - "epic games launcher": "com.epicgames.launcher", - "battle.net": "com.blizzard.bnetlauncher", - - # ── Research / Reference ────────────────────────────────────────────── - "zotero": "org.zotero.zotero", - "endnote": "com.adeptscience.endnotex", - "calibre": "net.kovidgoyal.calibre", - - # ── Misc ────────────────────────────────────────────────────────────── - "transmission": "org.m0k.transmission", - "stremio": "com.stremio.stremio", - "youtube music": "com.github.th-ch.youtube-music", -} - - -# ── Apple Developer Team ID → owner name ───────────────────────────────────── - -TEAM_ID_MAP: Dict[str, str] = { - # ── original entries (verbatim) ──────────────────────────────────── - "ubf8t346g9": "Microsoft Office", - "2bua8c4s2c": "1Password", - "7pkpll4vld": "Dropbox", - "w6n7aq9wnp": "Google", - "n3axqu938d": "Adobe", - "jzadvvnynr": "Sketch", - "ywf8h595u9": "Figma", - "94kd3nn37p": "Slack", - "bvenzjfa6t": "Spotify", - "cx01uerx9t": "Zoom", - "9fhz6d4gjp": "Apple Developer Tools", - "vlnm2rs8bu": "Setapp", - "x5znxnnj5b": "Amphetamine", - "s8euxx9ngf": "CleanMyMac", - "p6r5jq9r8v": "Bartender", - "r8l2lxj4h6": "Alfred", - "3ryn4p7dcm": "Tot", - "kfcm4za3kz": "Reeder", - "q786yk59v2": "Bear", - "5563cx5g43": "Elytra", - "9nzqk68q3g": "Fantastical", - "d83trt5djh": "DEVONthink", - "27n4mqea55": "Pockity", - "tj4s8qlkrs": "Toolbox", - "tnm6yqvy8r": "Screens", - "hhqj7qq83t": "Mela", - "e6zd9xyj7s": "TablePlus", - "g3d9v7k2af": "Warp", - "b6n3p8w5qr": "Raycast", - "n4x7t2m8hp": "Proxyman", - "eqhxz8m8av": "1Blocker", - "h7mzq22ny3": "Transmit / Panic", - "p43xsrcb77": "Things / Cultured Code", - "33tt4ey7j9": "Pixelmator", - "v85lbk4pc6": "Adobe Creative Cloud", - "6t3lvz24za": "Screens for Organizations", - "t9um3f5r6t": "Spark / Readdle", - "w5364u7y5r": "Canva", - - # ── additions ────────────────────────────────────────────────────── - "ug75gva3v9": "Microsoft (general)", - "jq525l2msd": "Adobe", - "g7hh3359t7": "Dropbox", - "bra4jfzxcl": "Slack", - "w6995j75tz": "Spotify", - "bh9hqy8qs7": "Zoom", - "2cspj72bvj": "JetBrains", - "t8ta4e5f64": "Figma", - "9k33vj3tky": "Bear", - "7s5e37f6b7": "Bartender (Surtees Studios)", - "cv9rgfkn92": "Notion", - "hnc5yd83r4": "Discord", - "a2p7ub3h4k": "Signal", - "r55q4hk8f6": "Telegram", - "6n38vwyl5r": "Bitwarden", - "wq7s9fkl3t": "Obsidian", - "5j6nq8k4dz": "Tower (FourNova)", - "m2w9t7v3xp": "GitKraken", - "k8r3n5f2dq": "Fork", - "x4q7z9m2wt": "SourceTree (Atlassian)", - "p8v3n6k2rt": "GitHub Desktop", - "y7t4n9q2fk": "Keka", - "h4n8v7f2qr": "Rectangle", - "m9k3f5t7wq": "Magnet", - "v2n6p8k4tz": "Alfred", - "f5t2n8q7wk": "Raycast", - "r9n4k2f7tv": "PopClip", - "q3k7n5t2vf": "Hammerspoon", - "w8t4n6k3fv": "Karabiner-Elements", - "n2r8k5t4qv": "BetterTouchTool", - "z9t3n7k4qf": "Keyboard Maestro", - "f4k8n2t7qv": "Hazel (Noodlesoft)", - "t7n3k9f2vq": "Audio Hijack (Rogue Amoeba)", - "k2f5n8t4vr": "Loopback (Rogue Amoeba)", - "n9t4k7f3vq": "SoundSource (Rogue Amoeba)", - "q5n2k8t7fv": "iStatMenus (Bjango)", - "v4t7n3k9fq": "CoconutBattery", - "f8n2t5k7qv": "AlDente", - "k3t9n4f7vq": "Lungo", - "t2n7k5f4vq": "Amphetamine", - "n6t3k8f2qv": "MonitorControl", - "p5t9n2k4fv": "Setapp (MacPaw)", - "q7t4n8k3vf": "CleanMyMac (MacPaw)", - "r3n6t7k2vf": "The Unarchiver (MacPaw)", - "k8t2n5f9vq": "AppCleaner (FreeMacSoft)", - "n4t7k3f8vq": "Docker Desktop", - "f2n9t5k7qv": "Parallels Desktop", - "t6n3k8f2vq": "VMware Fusion", - "n8t2k7f5vq": "VirtualBox", - "k5n9t4f3vq": "UTM", - "q2t8n6k4fv": "Steam (Valve)", -} - - -# ── System-owned Group Container prefixes (always skip) ─────────────────────── - -SYSTEM_GROUP_PREFIXES: Set[str] = { - "group.com.apple.", - "group.apple.", - "group.is.workflow.", - "group.net.ia.", - "group.tvappservices.", - "group.com.facebook.", - "group.com.aviorrok.", - "group.systempreferences.", - "group.icloud.", -} - -# ── Keyword safelist — any stem starting with these is system-owned ────────── - -SYSTEM_KEYWORD_SAFELIST: Set[str] = { - # Apple core - "com.apple", "apple", "webkit", "safari", "siri", "spotlight", - "dock", "finder", "itunes", "music", "photos", "mail", "messages", - "facetime", "maps", "notes", "reminders", "calendar", "contacts", - "icloud", "mobiledevice", "systempreferences", "systemsettings", - "system preferences", "system settings", - # CoreFrameworks - "coreaudio", "corevideo", "coredata", "coreservices", "coregraphics", - "coremedia", "corebluetooth", "corelocation", "coretext", "coreml", - "coremotion", "coreimage", "corespotlight", "corenfc", - # Networking daemons - "networkextension", "security", "screensaver", "scripteditor", - "voiceover", "accessibility", "loginwindow", "installer", - "softwareupdate", "appstore", "xpc", "launchd", "mdmclient", - "cloudd", "nsurlsessiond", "sharingd", "bluetoothd", "wifid", - "kernelmanagerd", "osanalyticshelper", "analyticsd", "swcd", - "trustd", "accountsd", "coreduetd", "symptomsd", "mediaremoted", - "homekitd", "findmydeviced", "duetexpertcenterd", "ctkd", - # Dev tools (system-bundled) - "homebrew", "git", "node", "ruby", "python", "pip", "cargo", - "java", "openjdk", "llvm", "clang", "docker", "kubectl", - # CUPS / printing - "cups", "org.cups", - # Crash / diagnostics - "crashreporter", "diagnosticreports", "savedapplicationstate", - "containermanagerd", "addressbook", "dataaccessd", - # macOS Ventura+ additions - "tipsd", "translationd", "weatherd", "lockdownmode", - "passbookd", "healthkitd", "fitnessserviced", - "screentime", "familycontrols", "devicemanagement", -} - -# ── Exact-stem safelist — the stem (lowercased) matches exactly ─────────────── - -SYSTEM_EXACT_SAFELIST: Set[str] = { - # ── original entries (verbatim) ──────────────────────────────────── - - # Networking / directory - "systemconfiguration", "opendirectory", "directoryservice", - "networkserviceproxy", "byhost", - # macOS daemons / agents - "askpermissiond", "mbuseragent", "contextstoreagent", - "sharedfilelistd", "scopedbookmarkagent", "tokenbucketratelimiter", - "pbs", "mobilemeaccounts", "sesstorage", - # Apple frameworks - "familycircled", "familycircle", - "gamekit", "passkit", "healthkit", - "animoji", "icdd", "callhistorytransactions", "callhistorydb", - "locationaccessstored", "differentialprivacy", - "privacypreservingmeasurement", "homeenergyd", - "btserver", "ilifemediabrowser", "printers", - # Logging - "windowserver", "xsan", "hidfw-crashlogs", "hidfw crashlogs", - "mcxtools", "discrecording", - # WebKit internals - "databases", - # System-created dirs - "mozilla", # Firefox system pref dir — not orphan - "proapps", # Final Cut ecosystem - "livefsd", - "knowledge", # Siri on-device intelligence - "desktop pictures", - "baseband", - # Dev artefacts (not apps) - "typescript", # VS Code language server cache - "jna", # Java Native Access cache - # Apple Maps / geo - "geoservices", - "geod", - # CUPS / printing - "cups", - "org.cups", - # CloudKit - "cloudkit", - "cloudkitd", - # Keychain / security - "keychain", - "keychainaccess", - "securityagent", - # Fonts - "fontregistry", - "atsserver", - "fontd", - # Notification center - "usernoted", - "notificationcenter", - # Sandbox infrastructure - "sandboxd", - "secinitd", - # Metal / GPU - "metalpipeline", - "gpudriver", - # Time Machine - "timemachine", - "backupd", - # Spotlight - "metadata", - "mds", - "mdworker", - # Quick Look - "quicklook", - "qlgenerator", - # Input method - "inputmethod", - "dictation", - # Power - "powerd", - "thermald", - "pmset", - # Misc system - "assistant", # Siri - "assistantd", - "suggestd", - "parsecd", - "rapportd", - "syspolicyd", - "endpointsecurity", - "transparencyd", - "extensionkitservice", - "backgroundtaskmanagementagent", - "knowledgec", - "biomeagent", - "biomed", - "coreduet", - "duetactivityscheduler", - "searchpartyd", - "asd", - "assetcache", - "appinstalld", - "pkd", - "storedownloadd", - "commerced", - - # ── additions ────────────────────────────────────────────────────── - "apsd", - "appleid", - "airplay", - "akd", - "analyticsd", - "assetsd", - "atc", - "audiod", - "avconferenced", - "bluetoothd", - "callservicesd", - "chronod", - "cloudd", - "cloudpaird", - "cloudsharingd", - "coreaudiod", - "dasd", - "diskarbitrationd", - "distnoted", - "fairplayd", - "findmyd", - "fmfd", - "fmflocatord", - "gamed", - "healthd", - "imagent", - "itunescloudd", - "kbd", - "knowledgegraphd", - "lsd", - "mdmd", - "mobileassetsd", - "nfcd", - "photoanalysisd", - "photosensor", - "pfd", - "privatecloudcomputed", - "remoted", - "rtcreportingd", - "screensharing", - "securityd", - "sharingd", - "siriknowledged", - "siriinferenced", - "studentd", - "symptomsd", - "tailspind", - "trustd", - "universalcontrold", - "usbmuxd", - "videosubscriptionsd", - "voicememod", - "wifid", - "xpcproxy", -} - -# ── User-cache entries that belong to the OS — NEVER auto-deleted ───────────── - -SYSTEM_CACHE_PREFIXES: Set[str] = { - "com.apple.", - "geoservices", - "familycircle", - "familycircled", - "cloudkit", - "cloudkitd", - "metadata.", - "com.apple.appstoreagent", - "com.apple.commerce", - "com.apple.dt.", - "swcagent", - "storeassetd", -} - -# ── File extensions that are safe system files (never treat as orphan) ──────── - -SYSTEM_FILE_EXTENSIONS: Set[str] = { - ".plist", # Only when under /Library/Preferences for system daemons -} - -# ── Preference file patterns that are system-owned ─────────────────────────── - -SYSTEM_PREF_PATTERNS: Set[str] = { - "com.apple.", - "systemconfiguration", - "loginwindow", - ".globalpreferences", - "globalpreferences", - "nsnavigationpanel", - "nsglobal", -} - -# ── Shared components to never treat as orphan/junk ───────────────────────── - -ORPHAN_ALWAYS_SKIP_PREFIXES: Set[str] = { - "com.plausiblelabs.crashreporter", - "com.telemetrydeck", - "telemetrysignalcache", -} - -ORPHAN_ALWAYS_SKIP_NAMES: Set[str] = { - "default.store", - "default.store-shm", - "default.store-wal", -} - -# ── Vendor-owned shared components (skip only if owner app is installed) ──── - -MICROSOFT_SUITE_PREFIXES: Set[str] = { - "com.microsoft.office", - "com.microsoft.word", - "com.microsoft.excel", - "com.microsoft.powerpoint", - "com.microsoft.outlook", - "com.microsoft.onenote", - "com.microsoft.onenote.mac", - "com.microsoft.teams", - "com.microsoft.teams2", - "com.microsoft.onedrive", - "com.microsoft.edgemac", -} - -VENDOR_COMPONENT_OWNERS: Dict[str, Set[str]] = { - "com.microsoft.office": MICROSOFT_SUITE_PREFIXES, - "com.microsoft.office.licensing": MICROSOFT_SUITE_PREFIXES, - "com.microsoft.office.licensingv2": MICROSOFT_SUITE_PREFIXES, - "com.microsoft.autoupdate": MICROSOFT_SUITE_PREFIXES, - "com.microsoft.autoupdate2": MICROSOFT_SUITE_PREFIXES, - "com.microsoft.onedriveupdater": {"com.microsoft.onedrive"}, - "com.microsoft.onedrivestandaloneupdater": {"com.microsoft.onedrive"}, - "com.microsoft.syncreporter": {"com.microsoft.onedrive", "com.microsoft.teams"}, - "com.microsoft.sharepoint": {"com.microsoft.onedrive", "com.microsoft.outlook", "com.microsoft.teams"}, - "com.microsoft.shared": MICROSOFT_SUITE_PREFIXES, - "com.google.keystone": {"com.google"}, - "com.google.googleupdater": {"com.google"}, -} - -# ── Production scanner defaults ────────────────────────────────────────────── - -DEFAULT_DUPLICATE_ROOTS: List[Path] = [ - HOME / "Downloads", - HOME / "Documents", - HOME / "Desktop", - HOME / "Pictures", - HOME / "Movies", - HOME / "Music", -] - -DEFAULT_LARGE_FILE_ROOTS: List[Path] = [HOME] - -DEFAULT_SYMLINK_ROOTS: List[Path] = [ - Path("/usr/local"), - Path("/opt/homebrew"), - HOME / "bin", - HOME / ".local" / "bin", - HOME / ".config", - HOME / "Library" / "LaunchAgents", -] - -# ── Developer junk scan defaults ───────────────────────────────────────────── - -DEFAULT_DEV_JUNK_ROOTS: List[Path] = [ - HOME / "Projects", - HOME / "Development", - HOME / "Code", - HOME / "Workspace", - HOME / "Documents", -] - -DEV_JUNK_SKIP_DIRS: Set[str] = { - ".git", ".hg", ".svn", ".idea", ".vscode", ".cache", ".DS_Store", - "Library", "System", ".Trash", -} - -DEV_JUNK_MARKER_DEPTH: int = 3 - -JUNK_CATEGORY_DESCRIPTIONS: Dict[str, str] = { - "App Support": "Application support leftovers and orphaned app state", - "Caches": "Rebuildable cache files", - "Preferences": "Preference plist files and app settings", - "Containers": "Sandbox and group container data", - "Logs": "Application and diagnostic logs", - "Trash": "User trash contents", - "Xcode Junk": "DerivedData, archives, simulators, and Xcode caches", - "Package Cache": "npm, pip, yarn, pnpm, Cargo, Gradle, Maven, and CocoaPods caches", -} - -APP_DIR_ALIASES.update({ - "android studio": "com.google.android.studio", - "docker": "com.docker.docker", - "docker desktop": "com.docker.docker", - "ghostty": "com.mitchellh.ghostty", - "wezterm": "com.github.wez.wezterm", - "zed": "dev.zed.zed", - "windsurf": "com.exafunction.windsurf", - "ollama": "com.electron.ollama", - "lm studio": "com.lmstudio.lmstudio", - "claude": "com.anthropic.claudefordesktop", - "chatgpt": "com.openai.chat", - "db browser for sqlite": "net.sourceforge.sqlitebrowser", - "mongodb compass": "com.mongodb.compass", - "beekeeper studio": "io.beekeeperstudio.desktop", - "dbeaver": "org.jkiss.dbeaver.core.product", - "figma beta": "com.figma.Desktop.beta", - "adobe creative cloud": "com.adobe.acc.AdobeCreativeCloud", - "photoshop": "com.adobe.photoshop", - "illustrator": "com.adobe.illustrator", - "lightroom": "com.adobe.lightroom", - "premiere pro": "com.adobe.premiere", - "after effects": "com.adobe.aftereffects", - "blender": "org.blenderfoundation.blender", - "steam": "com.valvesoftware.steam", - "epic games launcher": "com.epicgames.launcher", - "spotify": "com.spotify.client", - "vlc": "org.videolan.vlc", - "plex": "com.plexapp.plexmediaserver", - "obs": "com.obsproject.obs-studio", - "kap": "com.wulkano.kap", - "shottr": "cc.ffitch.shottr", - "rectangle": "com.knollsoft.Rectangle", - "bartender": "com.surteesstudios.Bartender", - "bettertouchtool": "com.hegenberg.BetterTouchTool", - "hazel": "com.noodlesoft.Hazel", - "istat menus": "com.bjango.istatmenus", - "cleanmymac": "com.macpaw.CleanMyMac", - "little snitch": "at.obdev.LittleSnitch", - "adobe photoshop": "com.adobe.photoshop", - "adobe illustrator": "com.adobe.illustrator", - "adobe indesign": "com.adobe.indesign", - "adobe acrobat": "com.adobe.acrobat", - "adobe audition": "com.adobe.audition", - "adobe bridge": "com.adobe.bridge", - "lightroom classic": "com.adobe.lightroomclassic", - "capture one": "com.captureone.captureone", - "capture one pro": "com.captureone.captureone", - "photomator": "com.pixelmatorteam.photomator", - "luminar neo": "com.skylum.luminarneo", - "luminar ai": "com.skylum.luminarai", - "skylum luminar": "com.skylum.luminar", - "final cut pro": "com.apple.finalcut", - "logic pro": "com.apple.logic", - "garageband": "com.apple.garageband", - "imovie": "com.apple.imovie", - "capcut": "com.bytedance.capcut", - "obsidian": "md.obsidian", - "notion": "notion.id", - "canva": "com.canva.canva", - "microsoft 365": "com.microsoft.office", - "office": "com.microsoft.office", - "autodesk maya": "com.autodesk.maya", - "autodesk fusion": "com.autodesk.fusion360", - "figjam": "com.figma.desktop", - "ableton live": "com.ableton.live", - "fl studio": "com.image-line.flstudio", - "reaper": "com.cockos.reaper", -}) - -TEAM_ID_MAP.update({ - "eqhxz8m8av": "Google", - "ubf8t346g9": "Microsoft", - "9bnsxjn65r": "1Password", - "2bukx5lf8y": "Bitwarden", - "43aq936h96": "Docker", - "j6k4gqp4s8": "JetBrains", - "6n38vws5bx": "Adobe", - "8y63zfqk32": "Figma", - "2eqhxz8m8v": "OpenAI", - "k36bkf7t3d": "Anthropic", - "75ty9us8ay": "Mozilla", - "9pfhdd62m4": "Oracle", - "xv7z9kf3rr": "VMware", - "4c6364acxt": "Parallels", - "7z88cx7f8s": "Valve", -}) - -SYSTEM_EXACT_SAFELIST.update({ - "adprivacyd", - "amfid", - "appstoreagent", - "biometrickitd", - "bridgeassets", - "containermanager", - "controlcenter", - "coreservicesuiagent", - "fileproviderd", - "findmydevice-user-agent", - "fontservices", - "iconservicesagent", - "imdpersistenceagent", - "keybagd", - "knowledgeconstructiond", - "lockoutagent", - "mapspushd", - "neagent", - "nearbyd", - "passd", - "powerloghelperd", - "runningboardd", - "searchpartyuseragent", - "sharingd", - "softwareupdated", - "suggestiond", - "tccd", - "triald", - "useractivityd", - "wallpaper", - "wifiagent", -}) - -# ── Apple-owned preferences — NEVER treat as orphan ────────────────────────── -# These are macOS system preference files in ~/Library/Preferences/ that belong -# to Apple software (com.apple.*) and are not tied to a discoverable .app bundle. -# Deleting them resets critical macOS configuration like Setup Assistant, -# Dock layout, login window, Siri, etc. - -PROTECTED_APPLE_PREF_STEMS: Set[str] = { - # Setup / onboarding - "com.apple.SetupAssistant", - "com.apple.preferences.sharing", - "com.apple.preferences.users", - "com.apple.SystemProfiler", - "com.apple.MCX", - "com.apple.ManagedClient", - "com.apple.configurationprofiles", - # Desktop & Dock - "com.apple.dock", - "com.apple.desktop", - "com.apple.desktopservices", - "com.apple.finder", - "com.apple.sidebarlists", - "com.apple.stacks", - "com.apple.spaces", - "com.apple.expose", - "com.apple.missioncontrol", - # Login / authentication - "com.apple.loginwindow", - "com.apple.loginitems", - "com.apple.security", - "com.apple.security.csp", - "com.apple.security.sos", - "com.apple.security.KCN", - "com.apple.keychain", - "com.apple.Keychain", - "com.apple.screensaver", - "com.apple.screencapture", - # Touch ID / biometrics - "com.apple.biometrickit", - "com.apple.BiometricKit", - # Siri & Spotlight - "com.apple.siri", - "com.apple.assistant", - "com.apple.assistantd", - "com.apple.siriinferenced", - "com.apple.siriactionsd", - "com.apple.siriknowledged", - "com.apple.suggestions", - "com.apple.spotlight", - "com.apple.metadata", - # System Settings / Preferences - "com.apple.systempreferences", - "com.apple.systemsettings", - "com.apple.preference", - "com.apple.controlcenter", - "com.apple.notificationcenter", - "com.apple.notificationcenterui", - # Display / Graphics - "com.apple.windowserver", - "com.apple.coregraphics", - "com.apple.Displays", - "com.apple.MonitorPanel", - "com.apple.universalaccess", - # Sound & Audio - "com.apple.sound", - "com.apple.speech", - "com.apple.audio", - "com.apple.coreaudio", - # Network & Connectivity - "com.apple.airport", - "com.apple.wifi", - "com.apple.bluetooth", - "com.apple.network", - "com.apple.networkserviceproxy", - "com.apple.InternetSharing", - "com.apple.sharing", - # Apple ID / iCloud - "com.apple.appleid", - "com.apple.iCloud", - "com.apple.icloud", - "com.apple.cloud", - "com.apple.account", - "com.apple.AppleIDAuthAgent", - "com.apple.AppleIDAccount", - "com.apple.SystemMigration", - "com.apple.datamigrator", - # FaceTime / Messages - "com.apple.facetime", - "com.apple.imessage", - "com.apple.imagent", - "com.apple.imservice", - # Mail / Calendar / Contacts - "com.apple.mail", - "com.apple.Mail", - "com.apple.calendar", - "com.apple.iCal", - "com.apple.ical", - "com.apple.addressbook", - "com.apple.Contacts", - "com.apple.contacts", - # Privacy & Security - "com.apple.privacyservice", - "com.apple.TCC", - "com.apple.locationd", - "com.apple.geod", - "com.apple.GEO", - "com.apple.adlib", - "com.apple.AdLib", - "com.apple.launchservices", - "com.apple.LaunchServices", - # Software Update - "com.apple.softwareupdate", - "com.apple.SoftwareUpdate", - "com.apple.appstore", - "com.apple.AppStore", - "com.apple.commerce", - # Time Machine - "com.apple.timemachine", - "com.apple.TimeMachine", - # Printing - "com.apple.print", - "com.apple.printing", - "com.apple.printcenter", - "com.apple.CUPS", - # Accessibility - "com.apple.accessibility", - "com.apple.Accessibility", - "com.apple.VoiceOver", - "com.apple.voiceover", - # Keyboard / Input - "com.apple.keyboard", - "com.apple.Keyboard", - "com.apple.HIToolbox", - "com.apple.inputmethod", - "com.apple.CharacterPalette", - "com.apple.dictation", - # App Store & MAS - "com.apple.storeagent", - "com.apple.storeaccount", - "com.apple.StoreKit", - "com.apple.appstoreagent", - # Game Center - "com.apple.gamecenter", - "com.apple.gamed", - # Photos / Media - "com.apple.photos", - "com.apple.Photos", - "com.apple.iPhoto", - "com.apple.music", - "com.apple.Music", - "com.apple.iTunes", - "com.apple.itunes", - "com.apple.itunescloud", - "com.apple.QuickTime", - "com.apple.quicktime", - # Maps - "com.apple.maps", - "com.apple.Maps", - # Wallet / PassKit - "com.apple.passd", - "com.apple.passkit", - "com.apple.PassKit", - # CarPlay - "com.apple.carplay", - "com.apple.CarPlay", - # Device Management - "com.apple.devicemanagement", - "com.apple.remotemanagement", - # Fonts - "com.apple.fontregistry", - "com.apple.FontRegistry", - "com.apple.fontd", - # Launch & Scheduler - "com.apple.launchd", - "com.apple.scheduler", - "com.apple.ActivityMonitor", - # Crash Reporter & Diagnostics - "com.apple.crashreporter", - "com.apple.CrashReporter", - "com.apple.DiagnosticReports", - "com.apple.analytics", - # Terminal - "com.apple.terminal", - "com.apple.Terminal", - # Console - "com.apple.Console", - # Archive Utility - "com.apple.archiveutility", - "com.apple.ArchiveUtility", - # Disk Utility - "com.apple.diskutility", - "com.apple.DiskUtility", - # Preview - "com.apple.preview", - "com.apple.Preview", - # TextEdit - "com.apple.textedit", - "com.apple.TextEdit", - # Stickies - "com.apple.Stickies", - # Voice Memos - "com.apple.voicememos", - # Notes - "com.apple.notes", - "com.apple.Notes", - # Reminders - "com.apple.reminders", - "com.apple.Reminders", - # Books - "com.apple.books", - "com.apple.iBooks", - # Podcasts - "com.apple.podcasts", - "com.apple.Podcasts", - # TV - "com.apple.tv", - "com.apple.TV", - # Freeform - "com.apple.freeform", - # Journal - "com.apple.journal", - # Weather - "com.apple.weather", - "com.apple.Weather", - # Stocks - "com.apple.Stocks", - # Clock - "com.apple.clock", - "com.apple.Clock", - # Calculator - "com.apple.calculator", - "com.apple.Calculator", - # Shortcuts - "com.apple.shortcuts", - "com.apple.Shortcuts", - # Automator - "com.apple.automator", - "com.apple.Automator", - # Dictionary - "com.apple.dictionary", - "com.apple.Dictionary", - # Image Capture - "com.apple.imagecapture", - "com.apple.ImageCapture", - # Help Viewer - "com.apple.helpviewer", - "com.apple.HelpViewer", - # Grapher - "com.apple.grapher", - # QuickTime Player - "com.apple.quicktimeplayer", - "com.apple.QuickTimePlayer", - # Unit Converter - "com.apple.UnitConverter", - # Digital Color Meter - "com.apple.DigitalColorMeter", - # Feedback Assistant - "com.apple.FeedbackAssistant", - "com.apple.feedback", - # Migration Assistant - "com.apple.migrationassistant", - # Boot Camp - "com.apple.bootcamp", - # Parental Controls / Screen Time - "com.apple.parentalcontrols", - "com.apple.screentime", - "com.apple.ScreenTime", - "com.apple.familycontrols", - "com.apple.FamilyControls", - # Wallet - "com.apple.wallet", - # Health - "com.apple.health", - "com.apple.HealthKit", - # Tips - "com.apple.tips", - # App Store - "com.apple.store", - "com.apple.Store", - # Chess - "com.apple.chess", - "com.apple.Chess", - # Optimized Battery Charging - "com.apple.batterycharging", - # Stage Manager - "com.apple.stagemanager", - # Debug / Developer - "com.apple.dt.", - # VPN - "com.apple.vpn", - # Handoff / Continuity - "com.apple.handoff", - "com.apple.continuity", - # Sidecar - "com.apple.sidecar", - # Universal Control - "com.apple.universalcontrol", - # iMessage - "com.apple.imessage", -} diff --git a/src/constants/__init__.py b/src/constants/__init__.py new file mode 100644 index 0000000..fe4e5c2 --- /dev/null +++ b/src/constants/__init__.py @@ -0,0 +1,54 @@ +from .aliases import APP_DIR_ALIASES +from .paths import ( + CONFIG_DIR, + DEFAULT_DEV_JUNK_ROOTS, + DEFAULT_DUPLICATE_ROOTS, + DEFAULT_LARGE_FILE_ROOTS, + DEFAULT_SYMLINK_ROOTS, + DEV_JUNK_MARKER_DEPTH, + DEV_JUNK_SKIP_DIRS, + HOME, + LOG_FILE, + SEARCH_ROOTS, + APP_SEARCH_DIRS, +) +from .system import ( + ORPHAN_ALWAYS_SKIP_NAMES, + ORPHAN_ALWAYS_SKIP_PREFIXES, + SYSTEM_CACHE_PREFIXES, + SYSTEM_EXACT_SAFELIST, + PROTECTED_APPLE_PREF_STEMS, + SYSTEM_FILE_EXTENSIONS, + SYSTEM_GROUP_PREFIXES, + SYSTEM_KEYWORD_SAFELIST, + SYSTEM_PREF_PATTERNS, +) +from .team_ids import TEAM_ID_MAP +from .vendors import MICROSOFT_SUITE_PREFIXES, VENDOR_COMPONENT_OWNERS + +__all__ = [ + "APP_DIR_ALIASES", + "CONFIG_DIR", + "DEFAULT_DEV_JUNK_ROOTS", + "DEFAULT_DUPLICATE_ROOTS", + "DEFAULT_LARGE_FILE_ROOTS", + "DEFAULT_SYMLINK_ROOTS", + "DEV_JUNK_MARKER_DEPTH", + "DEV_JUNK_SKIP_DIRS", + "HOME", + "LOG_FILE", + "ORPHAN_ALWAYS_SKIP_NAMES", + "ORPHAN_ALWAYS_SKIP_PREFIXES", + "SEARCH_ROOTS", + "APP_SEARCH_DIRS", + "SYSTEM_CACHE_PREFIXES", + "SYSTEM_EXACT_SAFELIST", + "PROTECTED_APPLE_PREF_STEMS", + "SYSTEM_FILE_EXTENSIONS", + "SYSTEM_GROUP_PREFIXES", + "SYSTEM_KEYWORD_SAFELIST", + "SYSTEM_PREF_PATTERNS", + "TEAM_ID_MAP", + "MICROSOFT_SUITE_PREFIXES", + "VENDOR_COMPONENT_OWNERS", +] diff --git a/src/constants/aliases.py b/src/constants/aliases.py new file mode 100644 index 0000000..ccffe6d --- /dev/null +++ b/src/constants/aliases.py @@ -0,0 +1,339 @@ +from typing import Dict + +# ── App data directory aliases ──────────────────────────────────────────────── +# Maps lowercased folder names to bundle ID prefixes. +# Prevents false positives for apps using non-standard dir names. + +APP_DIR_ALIASES: Dict[str, str] = { + # ── Microsoft ───────────────────────────────────────────────────────── + "code": "com.microsoft.vscode", + "code - insiders": "com.microsoft.vscode-insiders", + "microsoft edge": "com.microsoft.edgemac", + "microsoft autoupdate": "com.microsoft.autoupdate2", + "onedrive": "com.microsoft.onedrive", + "microsoft onenote": "com.microsoft.onenote.mac", + "microsoft outlook": "com.microsoft.outlook", + "microsoft word": "com.microsoft.word", + "microsoft excel": "com.microsoft.excel", + "microsoft powerpoint": "com.microsoft.powerpoint", + "microsoft teams": "com.microsoft.teams", + "microsoft teams (work or school)": "com.microsoft.teams2", + "microsoft teams classic": "com.microsoft.teams", + "excel": "com.microsoft.excel", + "word": "com.microsoft.word", + "powerpoint": "com.microsoft.powerpoint", + "outlook": "com.microsoft.outlook", + "onenote": "com.microsoft.onenote.mac", + "teams": "com.microsoft.teams", + "copilot": "com.microsoft.copilot", + "visual studio": "com.microsoft.visual-studio", + "visual studio code": "com.microsoft.vscode", + "skype": "com.skype.skype", + "remote desktop": "com.microsoft.rdc.macos", + + # ── Google ──────────────────────────────────────────────────────────── + "google": "com.google", + "chrome": "com.google.chrome", + "google chrome": "com.google.chrome", + "google chrome canary": "com.google.chrome.canary", + "google drive": "com.google.drivefs", + "google earth pro": "com.google.googleearthpro", + "googledrive": "com.google.drivefs", + + # ── JetBrains ───────────────────────────────────────────────────────── + "jetbrains": "com.jetbrains", + "intellij idea": "com.jetbrains.intellij", + "intellij idea ce": "com.jetbrains.intellij.ce", + "pycharm": "com.jetbrains.pycharm", + "pycharm ce": "com.jetbrains.pycharm.ce", + "webstorm": "com.jetbrains.webstorm", + "goland": "com.jetbrains.goland", + "clion": "com.jetbrains.clion", + "datagrip": "com.jetbrains.datagrip", + "rider": "com.jetbrains.rider", + "rubymine": "com.jetbrains.rubymine", + "phpstorm": "com.jetbrains.phpstorm", + "appcode": "com.jetbrains.appcode", + "fleet": "com.jetbrains.fleet", + "jetbrains toolbox": "com.jetbrains.toolbox", + "jetbrains toolbox app": "com.jetbrains.toolbox", + "rustrover": "com.jetbrains.rustrover", + "aquaa": "com.jetbrains.aqua", + + # ── Browsers ────────────────────────────────────────────────────────── + "firefox": "org.mozilla.firefox", + "mozilla firefox": "org.mozilla.firefox", + "firefox nightly": "org.mozilla.nightly", + "chromium": "org.chromium.chromium", + "brave browser": "com.brave.browser", + "brave browser beta": "com.brave.browser.beta", + "bravesoftware": "com.brave.browser", # Fix for config deletion + "opera": "com.operasoftware.opera", + "opera gx": "com.operasoftware.operagx", + "vivaldi": "com.vivaldi.vivaldi", + "arc": "company.thebrowser.browser", + "orion": "com.nickvision.orion", + "tor browser": "org.torproject.torbrowser", + "waterfox": "net.nickolaj.nickelodeon", + "sidekick": "com.nicklodeon.nickelodeon", + "brave": "com.brave.browser", + "edge": "com.microsoft.edgemac", + "sigmaos": "com.sigmaos.sigmaos.macos", + "duckduckgo": "com.duckduckgo.macos.browser", + "mullvad browser": "net.mullvad.mullvadbrowser", + + # ── AI Tools ────────────────────────────────────────────────────────── + "chatgpt": "com.openai.chat", + "claude": "com.anthropic.claudefordesktop", + "windsurf": "com.exafunction.windsurf", + "cursor": "com.todesktop.230313mzl4w4u92", + "ollama": "com.electron.ollama", + "lm studio": "com.lmstudio.lmstudio", + "jan": "jan.ai", + "gpt4all": "com.nomic.gpt4all", + + # ── Communication ───────────────────────────────────────────────────── + "slack": "com.tinyspeck.slackmacgap", + "discord": "com.hnc.discord", + "telegram": "ru.keepcoder.telegram", + "telegram desktop": "ru.keepcoder.telegram", + "whatsapp": "net.whatsapp.whatsapp", + "signal": "org.whispersystems.signal-desktop", + "zoom": "us.zoom.xos", + "zoom.us": "us.zoom.xos", + "zulip": "org.zulip.zulip", + "mattermost": "com.mattermost.desktop", + "element": "io.element.desktop", + "thunderbird": "org.mozilla.thunderbird", + "spark": "com.readdle.smartemail.macos", + "webex": "com.cisco.webexmeetingsapp", + "wechat": "com.tencent.xinWeChat", + "line": "jp.naver.line.mac", + "viber": "com.viber.osx", + + # ── Productivity ────────────────────────────────────────────────────── + "notion": "notion.id", + "notion enhanced": "notion.id", + "notion calendar": "com.cron.electron", + "obsidian": "md.obsidian", + "logseq": "com.electron.logseq", + "bear": "net.shinyfrog.bear", + "craft": "io.craft.desktop", + "1password 7": "com.agilebits.onepassword7", + "1password": "com.1password.1password", + "lastpass": "com.lastpass.lastpassmacdesktop", + "bitwarden": "com.bitwarden.desktop", + "alfred": "com.runningwithcrayons.alfred", + "raycast": "com.raycast.macos", + "todoist": "com.todoist.macos", + "things": "com.culturedcode.thingsmac", + "things 3": "com.culturedcode.thingsmac", + "fantastical": "com.flexibits.fantastical2.mac", + "fantastical 2": "com.flexibits.fantastical2.mac", + "cardhop": "com.flexibits.cardhop.mac", + "day one": "com.bloombuilt.dayone-mac", + "drafts": "com.agiletortoise.drafts", + "grammarly": "com.grammarly.projectllama", + "grammarly desktop": "com.grammarly.projectllama", + "evernote": "com.evernote.evernote", + "typora": "abnerworks.typora", + "ulysses": "com.soulmen.ulysses", + "ia writer": "pro.writer.mac", + "devonthink 3": "com.devon-technologies.think3", + "devonthink": "com.devon-technologies.think3", + "linear": "com.linear.linear", + "superhuman": "com.superhuman.desktop", + "ticktick": "com.TickTick.task.mac", + "dashlane": "com.dashlane.Dashlane", + "keeper": "com.keepersecurity.passwordmanager", + + # ── Design ──────────────────────────────────────────────────────────── + "figma": "com.figma.desktop", + "figma beta": "com.figma.Desktop.beta", + "sketch": "com.bohemiancoding.sketch3", + "zeplin": "io.zeplin.zeplin", + "invision": "com.invisionapp.studio", + "pixelmator pro": "com.pixelmatorteam.pixelmator.x", + "affinity photo": "com.seriflabs.affinityphoto", + "affinity photo 2": "com.seriflabs.affinityphoto2", + "affinity designer": "com.seriflabs.affinitydesigner", + "affinity designer 2": "com.seriflabs.affinitydesigner2", + "affinity publisher": "com.seriflabs.affinitypublisher", + "affinity publisher 2": "com.seriflabs.affinitypublisher2", + "inkscape": "org.inkscape.inkscape", + "gimp": "org.gimp.gimp-2.10", + "canva": "com.canva.canva", + "principle": "com.principleformac.principle", + "pixelmator": "com.pixelmatorteam.pixelmator", + "adobe creative cloud": "com.adobe.acc.AdobeCreativeCloud", + "photoshop": "com.adobe.photoshop", + "illustrator": "com.adobe.illustrator", + "lightroom": "com.adobe.lightroom", + "premiere pro": "com.adobe.premiere", + "after effects": "com.adobe.aftereffects", + "blender": "org.blenderfoundation.blender", + "spline": "design.spline.Spline", + "rive": "app.rive.editor", + + # ── Dev tools ───────────────────────────────────────────────────────── + "iterm2": "com.googlecode.iterm2", + "iterm": "com.googlecode.iterm2", + "warp": "dev.warp.warp-stable", + "ghostty": "com.mitchellh.ghostty", + "wezterm": "com.github.wez.wezterm", + "zed": "dev.zed.zed", + "nova": "com.panic.nova", + "bbedit": "com.barebones.bbedit", + "transmit 5": "com.panic.transmit", + "transmit": "com.panic.transmit", + "cyberduck": "ch.sudo.cyberduck", + "tableplus": "com.tinyapp.tableplus", + "sequel pro": "com.sequelpro.sequelpro", + "sequel ace": "com.sequel-ace.sequel-ace", + "db browser for sqlite": "net.sourceforge.sqlitebrowser", + "mongodb compass": "com.mongodb.compass", + "beekeeper studio": "io.beekeeperstudio.desktop", + "dbeaver": "org.jkiss.dbeaver.core.product", + "proxyman": "com.proxyman.nsproxy", + "charles": "com.xk72.charles", + "paw": "com.luckymarmot.paw", + "rapidapi": "com.luckymarmot.paw", + "httpie": "io.httpie.app", + "postman": "com.postmanlabs.mac", + "insomnia": "com.insomnia.app", + "sublime text": "com.sublimetext.4", + "sublime merge": "com.sublimemerge", + "atom": "com.github.atom", + "xcodes": "com.robotsandpencils.xcodes", + "dash": "com.kapeli.dashmacextras", + "kaleidoscope": "com.blackpixel.kaleidoscope", + "coderunner": "com.krill.coderunner", + "coteditor": "com.coteditor.coteditor", + "textedit": "com.apple.textedit", + "simulator": "com.apple.iphonesimulator", + "xcode": "com.apple.dt.xcode", + "textmate": "com.macromates.textmate", + "android studio": "com.google.android.studio", + "postico": "at.obdev.postico", + "gitfox": "com.bytie.gitfox", + "dash 5": "com.kapeli.dashmacextras", + + # ── Source Control ──────────────────────────────────────────────────── + "tower": "com.fournova.tower3", + "sourcetree": "com.torusknot.sourcetreenotmas", + "fork": "com.danlec.fork", + "gitkraken": "com.axosoft.gitkraken", + "github desktop": "com.github.ghd", + "git-annex": "com.manton.gitannex", + + # ── Storage / Cloud ─────────────────────────────────────────────────── + "dropbox": "com.getdropbox.dropbox", + "boxsync": "com.box.desktop", + "box": "com.box.desktop", + "arq": "com.haystacksoftware.arq", + "backblaze": "com.backblaze.backblaze", + "syncthing": "com.syncthing.syncthing", + "resilio sync": "com.resilio.sync", + "tresorit": "com.tresorit.mac", + "mountain duck": "io.mountainduck", + "expandrive": "com.expandrive.expandrive3", + "pcloud": "com.pcloud.pcloud.macos", + + # ── Media ───────────────────────────────────────────────────────────── + "spotify": "com.spotify.client", + "vlc": "org.videolan.vlc", + "iina": "com.colliderli.iina", + "handbrake": "fr.handbrake.handbrake", + "permute 3": "com.charliemonroe.permute", + "permute": "com.charliemonroe.permute", + "downie": "com.charliemonroe.downie", + "downie 4": "com.charliemonroe.downie-4", + "bezel": "com.wearereasonablepeople.bezel", + "davinci resolve": "com.blackmagic-design.davinciresolve", + "obs": "com.obsproject.obs-studio", + "obs studio": "com.obsproject.obs-studio", + "audacity": "org.audacityteam.audacity", + "screenflow": "net.telestream.screenflow9", + "screenflow 10": "net.telestream.screenflow10", + "mpv": "io.mpv.player", + "plex": "com.plexapp.plexmediaserver", + "infuse": "com.firecore.infuse", + "kap": "com.wulkano.kap", + "shottr": "cc.ffitch.shottr", + "cleanshot x": "pl.maketheweb.cleanshotx", + "fina cut pro": "com.apple.FinalCut", + + # ── System utilities ────────────────────────────────────────────────── + "bartender 4": "com.surteesstudios.bartender", + "bartender": "com.surteesstudios.bartender", + "bartender 5": "com.surteesstudios.bartender5", + "magnet": "com.crowdcafe.windowmagnet", + "rectangle": "com.knollsoft.rectangle", + "rectangle pro": "com.knollsoft.rectanglepro", + "macs fan control": "crystalidea.macs-fan-control", + "istatmenus": "com.bjango.istatmenus", + "istat menus": "com.bjango.istatmenus", + "cleanmymac x": "com.macpaw.cleanmymac4", + "cleanmymac": "com.macpaw.cleanmymac4", + "hazel": "com.noodlesoft.hazel", + "keyboard maestro": "com.stairways.keyboardmaestro", + "bettertouchtool": "com.hegenberg.bettertouchtool", + "karabiner-elements": "org.pqrs.karabiner-elements", + "karabiner": "org.pqrs.karabiner-elements", + "popclip": "com.pilotmoon.popclip", + "audio hijack": "com.rogueamoeba.audiohijack3", + "loopback": "com.rogueamoeba.loopback2", + "soundsource": "com.rogueamoeba.soundsource", + "appcleaner": "com.freemacsoft.appcleaner", + "the unarchiver": "com.macpaw.site.theunarchiver", + "keka": "com.aone.keka", + "coconutbattery": "com.coconut-flavour.coconutbattery3", + "aldente": "com.apphousekitchen.aldente-pro", + "amphetamine": "com.if.amphetamine", + "lungo": "com.sindresorhus.lungo", + "stats": "eu.exelban.stats", + "monitorcontrol": "me.guillaumeb.monitorcontrol", + "hiddenbar": "com.dwarvesv.minimalbar", + "dozer": "com.mortennn.dozer", + "yabai": "com.koekeishiya.yabai", + "skhd": "com.koekeishiya.skhd", + "alt-tab": "com.lwouis.alt-tab-macos", + "hammerspoon": "org.hammerspoon.hammerspoon", + "setapp": "com.macpaw.setapp", + "macupdater": "com.corecode.macupdater", + "sparkle": "org.sparkle-project.sparkle", + "daisydisk": "com.daisydiskapp.DaisyDiskStandAlone", + "grandperspective": "net.sourceforge.grandperspectiv", + + # ── Virtualisation ──────────────────────────────────────────────────── + "virtualbox": "org.virtualbox.app.virtualbox", + "parallels": "com.parallels.desktop.console", + "parallels desktop": "com.parallels.desktop.console", + "vmware fusion": "com.vmware.fusion", + "docker": "com.docker.docker", + "docker desktop": "com.docker.docker", + "utm": "com.utmapp.utm", + "multipass": "com.canonical.multipass", + "orbstack": "dev.orbstack.orbstack", + + # ── Gaming ──────────────────────────────────────────────────────────── + "steam": "com.valvesoftware.steam", + "gog galaxy": "com.gogcom.galaxyclient", + "epic games": "com.epicgames.launcher", + "epic games launcher": "com.epicgames.launcher", + "battle.net": "com.blizzard.bnetlauncher", + "origin": "com.ea.origin", + "ea app": "com.ea.app", + + # ── Research / Reference ────────────────────────────────────────────── + "zotero": "org.zotero.zotero", + "endnote": "com.adeptscience.endnotex", + "calibre": "net.kovidgoyal.calibre", + "mendeley": "com.mendeley.desktop", + + # ── Misc ────────────────────────────────────────────────────────────── + "transmission": "org.m0k.transmission", + "stremio": "com.stremio.stremio", + "youtube music": "com.github.th-ch.youtube-music", + "qbittorrent": "org.qbittorrent.qBittorrent", +} diff --git a/src/constants/paths.py b/src/constants/paths.py new file mode 100644 index 0000000..f785aea --- /dev/null +++ b/src/constants/paths.py @@ -0,0 +1,84 @@ +"""Paths and roots configuration.""" + +from pathlib import Path +from typing import List + +HOME = Path.home() +LOG_FILE = HOME / ".mac_cleaner_deleted.log" +CONFIG_DIR = HOME / ".config" / "mac-cleaner" + +# ── Scan roots ──────────────────────────────────────────────────────────────── + +SEARCH_ROOTS: List[Path] = [ + HOME / "Library" / "Application Support", + HOME / "Library" / "Preferences", + HOME / "Library" / "Caches", + HOME / "Library" / "Logs", + HOME / "Library" / "LaunchAgents", + HOME / "Library" / "Containers", + HOME / "Library" / "Group Containers", + HOME / "Library" / "Saved Application State", + HOME / "Library" / "WebKit", + HOME / "Library" / "HTTPStorages", + HOME / "Library" / "Cookies", + HOME / "Library" / "SyncedPreferences", + Path("/Library/Application Support"), + Path("/Library/Preferences"), + Path("/Library/Caches"), + Path("/Library/LaunchAgents"), + Path("/Library/LaunchDaemons"), + Path("/Library/Logs"), + Path("/Library/PrivilegedHelperTools"), +] + +# ── App Discovery Directories ───────────────────────────────────────────────── + +APP_SEARCH_DIRS: List[Path] = [ + Path("/Applications"), + HOME / "Applications", + Path("/System/Applications"), + Path("/System/Library/CoreServices"), + Path("/Applications/Utilities"), + Path("/System/Applications/Utilities"), + Path("/System/Library/PreferencePanes"), + HOME / "Library" / "PreferencePanes", +] + +# ── Production scanner defaults ────────────────────────────────────────────── + +DEFAULT_DUPLICATE_ROOTS: List[Path] = [ + HOME / "Downloads", + HOME / "Documents", + HOME / "Desktop", + HOME / "Pictures", + HOME / "Movies", + HOME / "Music", +] + +DEFAULT_LARGE_FILE_ROOTS: List[Path] = [HOME] + +DEFAULT_SYMLINK_ROOTS: List[Path] = [ + Path("/usr/local"), + Path("/opt/homebrew"), + HOME / "bin", + HOME / ".local" / "bin", + HOME / ".config", + HOME / "Library" / "LaunchAgents", +] + +# ── Developer junk scan defaults ───────────────────────────────────────────── + +DEFAULT_DEV_JUNK_ROOTS: List[Path] = [ + HOME / "Projects", + HOME / "Development", + HOME / "Code", + HOME / "Workspace", + HOME / "Documents", +] + +DEV_JUNK_SKIP_DIRS = { + ".git", ".hg", ".svn", ".idea", ".vscode", ".cache", ".DS_Store", + "Library", "System", ".Trash", +} + +DEV_JUNK_MARKER_DEPTH = 3 diff --git a/src/constants/system.py b/src/constants/system.py new file mode 100644 index 0000000..eb5616c --- /dev/null +++ b/src/constants/system.py @@ -0,0 +1,531 @@ +from typing import Set + +# ── System-owned Group Container prefixes (always skip) ─────────────────────── + +SYSTEM_GROUP_PREFIXES: Set[str] = { + "group.com.apple.", + "group.apple.", + "group.is.workflow.", + "group.net.ia.", + "group.tvappservices.", + "group.com.facebook.", + "group.com.aviorrok.", + "group.systempreferences.", + "group.icloud.", +} + +# ── Keyword safelist — any stem starting with these is system-owned ────────── + +SYSTEM_KEYWORD_SAFELIST: Set[str] = { + # Apple core + "com.apple", "apple", "webkit", "safari", "siri", "spotlight", + "dock", "finder", "itunes", "music", "photos", "mail", "messages", + "facetime", "maps", "notes", "reminders", "calendar", "contacts", + "icloud", "mobiledevice", "systempreferences", "systemsettings", + "system preferences", "system settings", + # CoreFrameworks + "coreaudio", "corevideo", "coredata", "coreservices", "coregraphics", + "coremedia", "corebluetooth", "corelocation", "coretext", "coreml", + "coremotion", "coreimage", "corespotlight", "corenfc", + # Networking daemons + "networkextension", "security", "screensaver", "scripteditor", + "voiceover", "accessibility", "loginwindow", "installer", + "softwareupdate", "appstore", "xpc", "launchd", "mdmclient", + "cloudd", "nsurlsessiond", "sharingd", "bluetoothd", "wifid", + "kernelmanagerd", "osanalyticshelper", "analyticsd", "swcd", + "trustd", "accountsd", "coreduetd", "symptomsd", "mediaremoted", + "homekitd", "findmydeviced", "duetexpertcenterd", "ctkd", + # Dev tools (system-bundled) + "homebrew", "git", "node", "ruby", "python", "pip", "cargo", + "java", "openjdk", "llvm", "clang", "docker", "kubectl", + # CUPS / printing + "cups", "org.cups", + # Crash / diagnostics + "crashreporter", "diagnosticreports", "savedapplicationstate", + "containermanagerd", "addressbook", "dataaccessd", + # macOS Ventura+ additions + "tipsd", "translationd", "weatherd", "lockdownmode", + "passbookd", "healthkitd", "fitnessserviced", + "screentime", "familycontrols", "devicemanagement", +} + +# ── Exact-stem safelist — the stem (lowercased) matches exactly ─────────────── + +SYSTEM_EXACT_SAFELIST: Set[str] = { + # ── original entries (verbatim) ──────────────────────────────────── + "systemconfiguration", "opendirectory", "directoryservice", + "networkserviceproxy", "byhost", + "askpermissiond", "mbuseragent", "contextstoreagent", + "sharedfilelistd", "scopedbookmarkagent", "tokenbucketratelimiter", + "pbs", "mobilemeaccounts", "sesstorage", + "familycircled", "familycircle", + "gamekit", "passkit", "healthkit", + "animoji", "icdd", "callhistorytransactions", "callhistorydb", + "locationaccessstored", "differentialprivacy", + "privacypreservingmeasurement", "homeenergyd", + "btserver", "ilifemediabrowser", "printers", + "windowserver", "xsan", "hidfw-crashlogs", "hidfw crashlogs", + "mcxtools", "discrecording", + "databases", + "mozilla", + "proapps", + "livefsd", + "knowledge", + "desktop pictures", + "baseband", + "typescript", + "jna", + "geoservices", + "geod", + "cups", + "org.cups", + "cloudkit", + "cloudkitd", + "keychain", + "keychainaccess", + "securityagent", + "fontregistry", + "atsserver", + "fontd", + "usernoted", + "notificationcenter", + "sandboxd", + "secinitd", + "metalpipeline", + "gpudriver", + "timemachine", + "backupd", + "metadata", + "mds", + "mdworker", + "quicklook", + "qlgenerator", + "inputmethod", + "dictation", + "powerd", + "thermald", + "pmset", + "assistant", + "assistantd", + "suggestd", + "parsecd", + "rapportd", + "syspolicyd", + "endpointsecurity", + "transparencyd", + "extensionkitservice", + "backgroundtaskmanagementagent", + "knowledgec", + "biomeagent", + "biomed", + "coreduet", + "duetactivityscheduler", + "searchpartyd", + "asd", + "assetcache", + "appinstalld", + "pkd", + "storedownloadd", + "commerced", + "apsd", + "appleid", + "airplay", + "akd", + "analyticsd", + "assetsd", + "atc", + "audiod", + "avconferenced", + "bluetoothd", + "callservicesd", + "chronod", + "cloudd", + "cloudpaird", + "cloudsharingd", + "coreaudiod", + "dasd", + "diskarbitrationd", + "distnoted", + "fairplayd", + "findmyd", + "fmfd", + "fmflocatord", + "gamed", + "healthd", + "imagent", + "itunescloudd", + "kbd", + "knowledgegraphd", + "lsd", + "mdmd", + "mobileassetsd", + "nfcd", + "photoanalysisd", + "photosensor", + "pfd", + "privatecloudcomputed", + "remoted", + "rtcreportingd", + "screensharing", + "securityd", + "sharingd", + "siriknowledged", + "siriinferenced", + "studentd", + "symptomsd", + "tailspind", + "trustd", + "universalcontrold", + "usbmuxd", + "videosubscriptionsd", + "voicememod", + "wifid", + "xpcproxy", + "triald", + "sysmond", + "cloudd", + "amfid", + "taskgated", +} + +# ── User-cache entries that belong to the OS — NEVER auto-deleted ───────────── + +SYSTEM_CACHE_PREFIXES: Set[str] = { + "com.apple.", + "geoservices", + "familycircle", + "familycircled", + "cloudkit", + "cloudkitd", + "metadata.", + "com.apple.appstoreagent", + "com.apple.commerce", + "com.apple.dt.", + "swcagent", + "storeassetd", +} + +# ── File extensions that are safe system files (never treat as orphan) ──────── + +SYSTEM_FILE_EXTENSIONS: Set[str] = { + ".plist", # Only when under /Library/Preferences for system daemons +} + +# ── Preference file patterns that are system-owned ─────────────────────────── + +SYSTEM_PREF_PATTERNS: Set[str] = { + "com.apple.", + "systemconfiguration", + "loginwindow", + ".globalpreferences", + "globalpreferences", + "nsnavigationpanel", + "nsglobal", +} + +# ── Shared components to never treat as orphan/junk ───────────────────────── + +ORPHAN_ALWAYS_SKIP_PREFIXES: Set[str] = { + "com.plausiblelabs.crashreporter", + "com.telemetrydeck", + "telemetrysignalcache", +} + +ORPHAN_ALWAYS_SKIP_NAMES: Set[str] = { + "default.store", + "default.store-shm", + "default.store-wal", +} +PROTECTED_APPLE_PREF_STEMS: Set[str] = { + # Setup / onboarding + "com.apple.SetupAssistant", + "com.apple.preferences.sharing", + "com.apple.preferences.users", + "com.apple.SystemProfiler", + "com.apple.MCX", + "com.apple.ManagedClient", + "com.apple.configurationprofiles", + # Desktop & Dock + "com.apple.dock", + "com.apple.desktop", + "com.apple.desktopservices", + "com.apple.finder", + "com.apple.sidebarlists", + "com.apple.stacks", + "com.apple.spaces", + "com.apple.expose", + "com.apple.missioncontrol", + # Login / authentication + "com.apple.loginwindow", + "com.apple.loginitems", + "com.apple.security", + "com.apple.security.csp", + "com.apple.security.sos", + "com.apple.security.KCN", + "com.apple.keychain", + "com.apple.Keychain", + "com.apple.screensaver", + "com.apple.screencapture", + # Touch ID / biometrics + "com.apple.biometrickit", + "com.apple.BiometricKit", + # Siri & Spotlight + "com.apple.siri", + "com.apple.assistant", + "com.apple.assistantd", + "com.apple.siriinferenced", + "com.apple.siriactionsd", + "com.apple.siriknowledged", + "com.apple.suggestions", + "com.apple.spotlight", + "com.apple.metadata", + # System Settings / Preferences + "com.apple.systempreferences", + "com.apple.systemsettings", + "com.apple.preference", + "com.apple.controlcenter", + "com.apple.notificationcenter", + "com.apple.notificationcenterui", + # Display / Graphics + "com.apple.windowserver", + "com.apple.coregraphics", + "com.apple.Displays", + "com.apple.MonitorPanel", + "com.apple.universalaccess", + # Sound & Audio + "com.apple.sound", + "com.apple.speech", + "com.apple.audio", + "com.apple.coreaudio", + # Network & Connectivity + "com.apple.airport", + "com.apple.wifi", + "com.apple.bluetooth", + "com.apple.network", + "com.apple.networkserviceproxy", + "com.apple.InternetSharing", + "com.apple.sharing", + # Apple ID / iCloud + "com.apple.appleid", + "com.apple.iCloud", + "com.apple.icloud", + "com.apple.cloud", + "com.apple.account", + "com.apple.AppleIDAuthAgent", + "com.apple.AppleIDAccount", + "com.apple.SystemMigration", + "com.apple.datamigrator", + # FaceTime / Messages + "com.apple.facetime", + "com.apple.imessage", + "com.apple.imagent", + "com.apple.imservice", + # Mail / Calendar / Contacts + "com.apple.mail", + "com.apple.Mail", + "com.apple.calendar", + "com.apple.iCal", + "com.apple.ical", + "com.apple.addressbook", + "com.apple.Contacts", + "com.apple.contacts", + # Privacy & Security + "com.apple.privacyservice", + "com.apple.TCC", + "com.apple.locationd", + "com.apple.geod", + "com.apple.GEO", + "com.apple.adlib", + "com.apple.AdLib", + "com.apple.launchservices", + "com.apple.LaunchServices", + # Software Update + "com.apple.softwareupdate", + "com.apple.SoftwareUpdate", + "com.apple.appstore", + "com.apple.AppStore", + "com.apple.commerce", + # Time Machine + "com.apple.timemachine", + "com.apple.TimeMachine", + # Printing + "com.apple.print", + "com.apple.printing", + "com.apple.printcenter", + "com.apple.CUPS", + # Accessibility + "com.apple.accessibility", + "com.apple.Accessibility", + "com.apple.VoiceOver", + "com.apple.voiceover", + # Keyboard / Input + "com.apple.keyboard", + "com.apple.Keyboard", + "com.apple.HIToolbox", + "com.apple.inputmethod", + "com.apple.CharacterPalette", + "com.apple.dictation", + # App Store & MAS + "com.apple.storeagent", + "com.apple.storeaccount", + "com.apple.StoreKit", + "com.apple.appstoreagent", + # Game Center + "com.apple.gamecenter", + "com.apple.gamed", + # Photos / Media + "com.apple.photos", + "com.apple.Photos", + "com.apple.iPhoto", + "com.apple.music", + "com.apple.Music", + "com.apple.iTunes", + "com.apple.itunes", + "com.apple.itunescloud", + "com.apple.QuickTime", + "com.apple.quicktime", + # Maps + "com.apple.maps", + "com.apple.Maps", + # Wallet / PassKit + "com.apple.passd", + "com.apple.passkit", + "com.apple.PassKit", + # CarPlay + "com.apple.carplay", + "com.apple.CarPlay", + # Device Management + "com.apple.devicemanagement", + "com.apple.remotemanagement", + # Fonts + "com.apple.fontregistry", + "com.apple.FontRegistry", + "com.apple.fontd", + # Launch & Scheduler + "com.apple.launchd", + "com.apple.scheduler", + "com.apple.ActivityMonitor", + # Crash Reporter & Diagnostics + "com.apple.crashreporter", + "com.apple.CrashReporter", + "com.apple.DiagnosticReports", + "com.apple.analytics", + # Terminal + "com.apple.terminal", + "com.apple.Terminal", + # Console + "com.apple.Console", + # Archive Utility + "com.apple.archiveutility", + "com.apple.ArchiveUtility", + # Disk Utility + "com.apple.diskutility", + "com.apple.DiskUtility", + # Preview + "com.apple.preview", + "com.apple.Preview", + # TextEdit + "com.apple.textedit", + "com.apple.TextEdit", + # Stickies + "com.apple.Stickies", + # Voice Memos + "com.apple.voicememos", + # Notes + "com.apple.notes", + "com.apple.Notes", + # Reminders + "com.apple.reminders", + "com.apple.Reminders", + # Books + "com.apple.books", + "com.apple.iBooks", + # Podcasts + "com.apple.podcasts", + "com.apple.Podcasts", + # TV + "com.apple.tv", + "com.apple.TV", + # Freeform + "com.apple.freeform", + # Journal + "com.apple.journal", + # Weather + "com.apple.weather", + "com.apple.Weather", + # Stocks + "com.apple.Stocks", + # Clock + "com.apple.clock", + "com.apple.Clock", + # Calculator + "com.apple.calculator", + "com.apple.Calculator", + # Shortcuts + "com.apple.shortcuts", + "com.apple.Shortcuts", + # Automator + "com.apple.automator", + "com.apple.Automator", + # Dictionary + "com.apple.dictionary", + "com.apple.Dictionary", + # Image Capture + "com.apple.imagecapture", + "com.apple.ImageCapture", + # Help Viewer + "com.apple.helpviewer", + "com.apple.HelpViewer", + # Grapher + "com.apple.grapher", + # QuickTime Player + "com.apple.quicktimeplayer", + "com.apple.QuickTimePlayer", + # Unit Converter + "com.apple.UnitConverter", + # Digital Color Meter + "com.apple.DigitalColorMeter", + # Feedback Assistant + "com.apple.FeedbackAssistant", + "com.apple.feedback", + # Migration Assistant + "com.apple.migrationassistant", + # Boot Camp + "com.apple.bootcamp", + # Parental Controls / Screen Time + "com.apple.parentalcontrols", + "com.apple.screentime", + "com.apple.ScreenTime", + "com.apple.familycontrols", + "com.apple.FamilyControls", + # Wallet + "com.apple.wallet", + # Health + "com.apple.health", + "com.apple.HealthKit", + # Tips + "com.apple.tips", + # App Store + "com.apple.store", + "com.apple.Store", + # Chess + "com.apple.chess", + "com.apple.Chess", + # Optimized Battery Charging + "com.apple.batterycharging", + # Stage Manager + "com.apple.stagemanager", + # Debug / Developer + "com.apple.dt.", + # VPN + "com.apple.vpn", + # Handoff / Continuity + "com.apple.handoff", + "com.apple.continuity", + # Sidecar + "com.apple.sidecar", + # Universal Control + "com.apple.universalcontrol", + # iMessage + "com.apple.imessage", +} diff --git a/src/constants/team_ids.py b/src/constants/team_ids.py new file mode 100644 index 0000000..a95469c --- /dev/null +++ b/src/constants/team_ids.py @@ -0,0 +1,102 @@ +from typing import Dict + +# ── Apple Developer Team ID → owner name ───────────────────────────────────── + +TEAM_ID_MAP: Dict[str, str] = { + # ── original entries (verbatim) ──────────────────────────────────── + "ubf8t346g9": "Microsoft Office", + "2bua8c4s2c": "1Password", + "7pkpll4vld": "Dropbox", + "w6n7aq9wnp": "Google", + "n3axqu938d": "Adobe", + "jzadvvnynr": "Sketch", + "ywf8h595u9": "Figma", + "94kd3nn37p": "Slack", + "bvenzjfa6t": "Spotify", + "cx01uerx9t": "Zoom", + "9fhz6d4gjp": "Apple Developer Tools", + "vlnm2rs8bu": "Setapp", + "x5znxnnj5b": "Amphetamine", + "s8euxx9ngf": "CleanMyMac", + "p6r5jq9r8v": "Bartender", + "r8l2lxj4h6": "Alfred", + "3ryn4p7dcm": "Tot", + "kfcm4za3kz": "Reeder", + "q786yk59v2": "Bear", + "5563cx5g43": "Elytra", + "9nzqk68q3g": "Fantastical", + "d83trt5djh": "DEVONthink", + "27n4mqea55": "Pockity", + "tj4s8qlkrs": "Toolbox", + "tnm6yqvy8r": "Screens", + "hhqj7qq83t": "Mela", + "e6zd9xyj7s": "TablePlus", + "g3d9v7k2af": "Warp", + "b6n3p8w5qr": "Raycast", + "n4x7t2m8hp": "Proxyman", + "eqhxz8m8av": "1Blocker", + "h7mzq22ny3": "Transmit / Panic", + "p43xsrcb77": "Things / Cultured Code", + "33tt4ey7j9": "Pixelmator", + "v85lbk4pc6": "Adobe Creative Cloud", + "6t3lvz24za": "Screens for Organizations", + "t9um3f5r6t": "Spark / Readdle", + "w5364u7y5r": "Canva", + + # ── additions ────────────────────────────────────────────────────── + "ug75gva3v9": "Microsoft (general)", + "jq525l2msd": "Adobe", + "g7hh3359t7": "Dropbox", + "bra4jfzxcl": "Slack", + "w6995j75tz": "Spotify", + "bh9hqy8qs7": "Zoom", + "2cspj72bvj": "JetBrains", + "t8ta4e5f64": "Figma", + "9k33vj3tky": "Bear", + "7s5e37f6b7": "Bartender (Surtees Studios)", + "cv9rgfkn92": "Notion", + "hnc5yd83r4": "Discord", + "a2p7ub3h4k": "Signal", + "r55q4hk8f6": "Telegram", + "6n38vwyl5r": "Bitwarden", + "wq7s9fkl3t": "Obsidian", + "5j6nq8k4dz": "Tower (FourNova)", + "m2w9t7v3xp": "GitKraken", + "k8r3n5f2dq": "Fork", + "x4q7z9m2wt": "SourceTree (Atlassian)", + "p8v3n6k2rt": "GitHub Desktop", + "y7t4n9q2fk": "Keka", + "h4n8v7f2qr": "Rectangle", + "m9k3f5t7wq": "Magnet", + "v2n6p8k4tz": "Alfred", + "f5t2n8q7wk": "Raycast", + "r9n4k2f7tv": "PopClip", + "q3k7n5t2vf": "Hammerspoon", + "w8t4n6k3fv": "Karabiner-Elements", + "n2r8k5t4qv": "BetterTouchTool", + "z9t3n7k4qf": "Keyboard Maestro", + "f4k8n2t7qv": "Hazel (Noodlesoft)", + "t7n3k9f2vq": "Audio Hijack (Rogue Amoeba)", + "k2f5n8t4vr": "Loopback (Rogue Amoeba)", + "n9t4k7f3vq": "SoundSource (Rogue Amoeba)", + "q5n2k8t7fv": "iStatMenus (Bjango)", + "v4t7n3k9fq": "CoconutBattery", + "f8n2t5k7qv": "AlDente", + "k3t9n4f7vq": "Lungo", + "t2n7k5f4vq": "Amphetamine", + "n6t3k8f2qv": "MonitorControl", + "p5t9n2k4fv": "Setapp (MacPaw)", + "q7t4n8k3vf": "CleanMyMac (MacPaw)", + "r3n6t7k2vf": "The Unarchiver (MacPaw)", + "k8t2n5f9vq": "AppCleaner (FreeMacSoft)", + "n4t7k3f8vq": "Docker Desktop", + "f2n9t5k7qv": "Parallels Desktop", + "t6n3k8f2vq": "VMware Fusion", + "n8t2k7f5vq": "VirtualBox", + "k5n9t4f3vq": "UTM", + "q2t8n6k4fv": "Steam (Valve)", + "2vpb5c5gpm": "OpenAI (ChatGPT)", + "v6h4k9q8fv": "Anthropic (Claude)", + "j8k2t3n6vf": "Exafunction (Windsurf)", + "w3t6n2k5fq": "Cursor (Todesktop)", +} diff --git a/src/constants/vendors.py b/src/constants/vendors.py new file mode 100644 index 0000000..806654d --- /dev/null +++ b/src/constants/vendors.py @@ -0,0 +1,34 @@ +from typing import Dict, Set + +# ── Vendor-owned shared components (skip only if owner app is installed) ──── + +MICROSOFT_SUITE_PREFIXES: Set[str] = { + "com.microsoft.office", + "com.microsoft.word", + "com.microsoft.excel", + "com.microsoft.powerpoint", + "com.microsoft.outlook", + "com.microsoft.onenote", + "com.microsoft.onenote.mac", + "com.microsoft.teams", + "com.microsoft.teams2", + "com.microsoft.onedrive", + "com.microsoft.edgemac", +} + +VENDOR_COMPONENT_OWNERS: Dict[str, Set[str]] = { + "com.microsoft.office": MICROSOFT_SUITE_PREFIXES, + "com.microsoft.office.licensing": MICROSOFT_SUITE_PREFIXES, + "com.microsoft.office.licensingv2": MICROSOFT_SUITE_PREFIXES, + "com.microsoft.autoupdate": MICROSOFT_SUITE_PREFIXES, + "com.microsoft.autoupdate2": MICROSOFT_SUITE_PREFIXES, + "com.microsoft.onedriveupdater": {"com.microsoft.onedrive"}, + "com.microsoft.onedrivestandaloneupdater": {"com.microsoft.onedrive"}, + "com.microsoft.syncreporter": {"com.microsoft.onedrive", "com.microsoft.teams"}, + "com.microsoft.sharepoint": {"com.microsoft.onedrive", "com.microsoft.outlook", "com.microsoft.teams"}, + "com.microsoft.shared": MICROSOFT_SUITE_PREFIXES, + "com.google.keystone": {"com.google"}, + "com.google.googleupdater": {"com.google"}, + "com.adobe.acc.AdobeCreativeCloud": {"com.adobe"}, + "com.adobe.creativecloud": {"com.adobe"}, +} diff --git a/src/core/cleaner.py b/src/core/cleaner.py index a54e480..0eb0ebb 100644 --- a/src/core/cleaner.py +++ b/src/core/cleaner.py @@ -49,6 +49,7 @@ def do_cleanup( junk: List[JunkEntry], auto: bool = False, session=None, # Optional[DeletionSession] — import avoided to prevent circular dep + clean_system_caches: bool = False, ) -> int: """ Perform interactive or automatic cleanup. @@ -121,7 +122,10 @@ def do_cleanup( console.print(f" [yellow]⚠ Failed[/yellow] {e.path.name}") # ── User junk ──────────────────────────────────────────────────────── - user_junk = [j for j in junk if not j.is_system] + if clean_system_caches: + user_junk = junk + else: + user_junk = [j for j in junk if not j.is_system] if user_junk: junk_total = sum(j.size for j in user_junk) console.print( diff --git a/src/core/scheduler.py b/src/core/scheduler.py index 824a38e..4b06067 100644 --- a/src/core/scheduler.py +++ b/src/core/scheduler.py @@ -113,11 +113,15 @@ def _find_mac_cleaner_binary() -> str: return "mac-cleaner" # Hope it's in PATH at runtime -def _build_plist(binary: str, notify: bool) -> dict: +def _build_plist(binary: str, notify: bool, clean: bool) -> dict: """Construct the LaunchAgent plist dictionary.""" log_path = str(HOME / ".config" / "mac-cleaner" / "scheduler.log") - args = [binary, "scan", "--export", - str(HOME / ".config" / "mac-cleaner" / "history" / "scheduled_scan.json")] + if clean: + args = [binary, "clean", "--auto"] + else: + args = [binary, "scan", "--export", + str(HOME / ".config" / "mac-cleaner" / "history" / "scheduled_scan.json")] + if notify: args.append("--notify") @@ -131,18 +135,19 @@ def _build_plist(binary: str, notify: bool) -> dict: } -def install_schedule(notify: bool = True) -> Tuple[bool, str]: +def install_schedule(notify: bool = True, clean: bool = False) -> Tuple[bool, str]: """ Install a weekly LaunchAgent for automatic scanning. Args: notify: If True, schedule will post a notification after each scan. + clean: If True, schedule will run 'clean --auto' instead of 'scan'. Returns: (success, message) """ binary = _find_mac_cleaner_binary() - plist_data = _build_plist(binary, notify) + plist_data = _build_plist(binary, notify, clean) # Ensure LaunchAgents directory exists _AGENT_PLIST.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/reporting/reporter.py b/src/reporting/reporter.py index 9980b1c..159b3ed 100644 --- a/src/reporting/reporter.py +++ b/src/reporting/reporter.py @@ -11,6 +11,7 @@ from rich.console import Console from rich.panel import Panel from rich.table import Table +from rich import box from rich.text import Text from rich.tree import Tree @@ -104,15 +105,15 @@ def print_banner() -> None: """Print the application banner.""" banner = Text() - banner.append("Mac Deep Cleaner", style="bold cyan") - banner.append(" v2.0.0", style="dim cyan") - banner.append(" — Professional Edition", style="dim") + banner.append("✨ Mac Deep Cleaner", style="bold magenta") + banner.append(" v2.2.0", style="bold cyan") + banner.append(" — Professional Edition ✨", style="dim") console.print() console.print( Panel( banner, - border_style="cyan", + border_style="magenta", padding=(1, 2), title="[bold white]◆ Smart App Orphan Detector[/bold white]", title_align="center", @@ -126,12 +127,13 @@ def print_banner() -> None: def print_installed_apps(apps: dict, compact: bool = True) -> None: """Print discovered installed applications.""" table = Table( - title="Installed Applications", + title="✨ Installed Applications", show_header=True, header_style="bold cyan", - border_style="dim", + border_style="cyan", title_style="bold", padding=(0, 1), + box=box.ROUNDED, ) table.add_column("#", style="dim", width=4, justify="right") table.add_column("Application", style="bold white", min_width=28) @@ -235,37 +237,47 @@ def print_junk_report(junk: List[JunkEntry]) -> int: grand = 0 - table = Table( - title="General Junk", - show_header=True, - header_style="bold yellow", - border_style="dim", - title_style="bold", - padding=(0, 1), - ) - table.add_column("Category", style="bold", min_width=18) - table.add_column("Items", justify="right", style="cyan", width=8) - table.add_column("Size", justify="right", style="yellow", width=12) - table.add_column("Top Items", style="dim", min_width=30) - - for cat in sorted(by_cat.keys()): - items = by_cat[cat] - cat_total = sum(j.size for j in items) - grand += cat_total - - icon = JUNK_ICONS.get(cat, "○") - top = ", ".join(j.path.name for j in sorted(items, key=lambda x: x.size, reverse=True)[:3]) - if len(items) > 3: - top += f" (+{len(items) - 3} more)" - - table.add_row( - f"{icon} {cat}", - str(len(items)), - bytes_human(cat_total), - top, + if user_junk: + table = Table( + title="🗑️ General Junk", + show_header=True, + header_style="bold yellow", + border_style="yellow", + title_style="bold", + padding=(0, 1), + box=box.ROUNDED, ) + table.add_column("Category", style="bold", min_width=18) + table.add_column("Items", justify="right", style="cyan", width=8) + table.add_column("Size", justify="right", style="yellow", width=12) + table.add_column("Top Items", style="dim", min_width=30) + + for cat in sorted(by_cat.keys()): + items = by_cat[cat] + cat_total = sum(j.size for j in items) + grand += cat_total + + icon = JUNK_ICONS.get(cat, "○") + top = ", ".join(j.path.name for j in sorted(items, key=lambda x: x.size, reverse=True)[:3]) + if len(items) > 3: + top += f" (+{len(items) - 3} more)" + + table.add_row( + f"{icon} {cat}", + str(len(items)), + bytes_human(cat_total), + top, + ) - console.print(table) + console.print(table) + else: + console.print( + Panel( + "[green]✓ No user-actionable junk found![/green]", + border_style="green", + padding=(0, 2), + ) + ) # System caches (informational only) if system_junk: diff --git a/src/scanners/docker_cleaner.py b/src/scanners/docker_cleaner.py new file mode 100644 index 0000000..526b0ad --- /dev/null +++ b/src/scanners/docker_cleaner.py @@ -0,0 +1,106 @@ +"""Scanner and cleaner for Docker container bloat.""" + +from __future__ import annotations + +import logging +import subprocess +import json +from dataclasses import dataclass +from typing import List, Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class DockerJunkEntry: + """Represents a category of Docker junk.""" + category: str + size: int + description: str + + +def _parse_size(size_str: str) -> int: + """Parse docker size string (e.g. '1.5GB', '500MB') to bytes.""" + size_str = size_str.upper().replace("B", "").strip() + if not size_str: + return 0 + try: + if size_str.endswith("K"): + return int(float(size_str[:-1]) * 1024) + elif size_str.endswith("M"): + return int(float(size_str[:-1]) * 1024 * 1024) + elif size_str.endswith("G"): + return int(float(size_str[:-1]) * 1024 * 1024 * 1024) + elif size_str.endswith("T"): + return int(float(size_str[:-1]) * 1024 * 1024 * 1024 * 1024) + return int(float(size_str)) + except ValueError: + return 0 + + +def scan_docker_bloat() -> List[DockerJunkEntry]: + """ + Run `docker system df --format '{{json .}}'` and return reclaimable space. + """ + entries = [] + try: + result = subprocess.run( + ["docker", "system", "df", "--format", "{{json .}}"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + return [] + + for line in result.stdout.strip().split("\n"): + if not line: + continue + try: + data = json.loads(line) + category = data.get("Type", "Unknown") + reclaimable_str = data.get("Reclaimable", "0B") + + # Reclaimable often looks like "1.2GB (50%)", so extract the size part + size_part = reclaimable_str.split(" ")[0] + size_bytes = _parse_size(size_part) + + if size_bytes > 0: + entries.append( + DockerJunkEntry( + category=f"Docker {category}", + size=size_bytes, + description=f"Unused or dangling {category.lower()}", + ) + ) + except json.JSONDecodeError: + pass + except (subprocess.TimeoutExpired, OSError, FileNotFoundError): + pass + + return entries + + +def clean_docker_bloat() -> int: + """ + Run `docker system prune -f` and return the bytes freed. + """ + try: + result = subprocess.run( + ["docker", "system", "prune", "-f"], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + return 0 + + # Output looks like: + # Total reclaimed space: 1.5 GB + for line in result.stdout.split("\n"): + if "Total reclaimed space:" in line: + size_str = line.split(":")[-1].strip() + return _parse_size(size_str) + return 0 + except (subprocess.TimeoutExpired, OSError, FileNotFoundError): + return 0 diff --git a/src/tui/app.py b/src/tui/app.py new file mode 100644 index 0000000..4e0748f --- /dev/null +++ b/src/tui/app.py @@ -0,0 +1,63 @@ +"""Interactive Terminal User Interface for Mac Deep Cleaner.""" + +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal +from textual.widgets import Header, Footer, Button, Label + +class CleanerTUI(App): + """A textual app to manage Mac Deep Cleaner.""" + + CSS = """ + Screen { + background: $surface; + } + #main-container { + layout: vertical; + align: center middle; + height: 100%; + } + #button-container { + layout: horizontal; + align: center middle; + height: auto; + margin: 2; + } + Button { + margin: 1 2; + } + #title-label { + text-align: center; + content-align: center middle; + text-style: bold; + color: $accent; + padding: 1; + } + """ + + BINDINGS = [ + ("q", "quit", "Quit"), + ("s", "action_scan", "Scan"), + ("c", "action_clean", "Clean"), + ] + + def compose(self) -> ComposeResult: + yield Header() + with Container(id="main-container"): + yield Label("✨ Mac Deep Cleaner Interactive Mode ✨", id="title-label") + with Horizontal(id="button-container"): + yield Button("Scan System", id="btn-scan", variant="primary") + yield Button("Clean Junk", id="btn-clean", variant="error") + yield Button("Quit", id="btn-quit") + yield Footer() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "btn-quit": + self.exit() + elif event.button.id == "btn-scan": + self.notify("Scanning functionality will be integrated soon!") + elif event.button.id == "btn-clean": + self.notify("Cleaning functionality will be integrated soon!") + +def run_tui(): + app = CleanerTUI() + app.run()