Первый выпуск SysDevCleaner

Анализ Windows и очистка кешей, мусора и остатков разработки.

Ядро:
- каталог правил: 228 правил путей (496 шаблонов) + 43 правила артефактов
  проектов — Windows, браузеры, кеши сборки, IDE, приложения, пакетные
  менеджеры;
- рекурсивный поиск node_modules, target, build, __pycache__, .venv и т.п.
  с проверкой маркеров проекта, чтобы не ловить ложные срабатывания;
- поиск крупных файлов и дубликатов (хеш по началу/середине/концу и полная
  сверка), инвентаризация установленных программ через реестр;
- ограничение области сканирования по дискам;
- рекомендации и текстовый отчёт.

Безопасность:
- четыре уровня риска, автоматически отмечается только «Безопасно»;
- список защищённых путей проверяется и при выборе, и перед удалением;
- для каталогов вроде %TEMP% удаляется содержимое, а не сам каталог;
- удаление в Корзину по умолчанию, пробный запуск, журнал очисток.

Интерфейс на Dear PyGui:
- JetBrains Mono Nerd Font: кириллица и иконки в одном шрифте;
- тёмная и светлая темы, адаптивная раскладка;
- заголовок окна ставится через SetWindowTextW — Dear PyGui отдаёт его
  в ANSI-функцию как UTF-8 и портит кириллицу;
- запуск всегда с правами администратора: манифест UAC в exe и
  самоперезапуск через ShellExecuteW при работе из исходников.

Сборка в один exe через PyInstaller со вшитым шрифтом и значком.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:55:00 +03:00
commit b01cf37e66
33 changed files with 6472 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
"""Инвентаризация установленных программ: размер на диске, давность использования."""
from __future__ import annotations
import os
import subprocess
import sys
from typing import Callable
from .model import Program
from .util import Cancel, dir_stats, known_paths
_UNINSTALL_KEYS = (
("HKLM", r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM"),
("HKLM", r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM-32"),
("HKCU", r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKCU"),
)
def _read_registry() -> list[Program]:
if sys.platform != "win32":
return []
import winreg
roots = {"HKLM": winreg.HKEY_LOCAL_MACHINE, "HKCU": winreg.HKEY_CURRENT_USER}
out: list[Program] = []
seen: set[tuple[str, str]] = set()
for root_name, subkey, label in _UNINSTALL_KEYS:
try:
key = winreg.OpenKey(roots[root_name], subkey, 0, winreg.KEY_READ)
except OSError:
continue
with key:
try:
count = winreg.QueryInfoKey(key)[0]
except OSError:
continue
for i in range(count):
try:
name = winreg.EnumKey(key, i)
sub = winreg.OpenKey(key, name, 0, winreg.KEY_READ)
except OSError:
continue
with sub:
def val(field: str, default=""):
try:
return winreg.QueryValueEx(sub, field)[0]
except OSError:
return default
display = str(val("DisplayName") or "").strip()
if not display:
continue
if val("SystemComponent", 0) == 1:
continue
if val("ParentKeyName") or val("ParentDisplayName"):
# обновления и компоненты внутри бандлов
if not val("UninstallString"):
continue
version = str(val("DisplayVersion") or "")
dedup = (display.lower(), version)
if dedup in seen:
continue
seen.add(dedup)
try:
est = int(val("EstimatedSize", 0) or 0) * 1024
except (TypeError, ValueError):
est = 0
out.append(Program(
name=display,
publisher=str(val("Publisher") or ""),
version=version,
install_date=str(val("InstallDate") or ""),
install_location=str(val("InstallLocation") or "").strip('"'),
uninstall_cmd=str(val("QuietUninstallString")
or val("UninstallString") or ""),
registry_size=est,
source=label,
))
return out
def _guess_location(prog: Program) -> str:
"""Если InstallLocation пуст — пробуем вытащить путь из строки удаления."""
if prog.install_location and os.path.isdir(prog.install_location):
return prog.install_location
cmd = prog.uninstall_cmd
if not cmd:
return ""
candidate = ""
if cmd.startswith('"'):
candidate = cmd[1:].split('"', 1)[0]
else:
parts = cmd.split(" ")
for i in range(len(parts), 0, -1):
guess = " ".join(parts[:i])
if os.path.exists(guess):
candidate = guess
break
if not candidate:
return ""
folder = os.path.dirname(candidate)
k = known_paths()
# не отдаём общие корни вроде C:\Windows\System32
bad = {os.path.normcase(k["WINDIR"]), os.path.normcase(os.path.join(k["WINDIR"], "System32")),
os.path.normcase(k["PROGRAMFILES"]), os.path.normcase(k["PROGRAMFILES86"]),
os.path.normcase(k["TEMP"])}
if not folder or os.path.normcase(folder) in bad or len(folder) <= 3:
return ""
return folder if os.path.isdir(folder) else ""
def scan_programs(
measure_disk: bool = True,
cancel: Cancel | None = None,
progress: Callable[[str], None] | None = None,
) -> list[Program]:
"""Список установленных программ, отсортированный по занимаемому месту."""
progs = _read_registry()
if measure_disk:
for i, p in enumerate(progs):
if cancel is not None and cancel.cancelled:
break
loc = _guess_location(p)
if not loc:
continue
p.install_location = loc
size, _files, _dirs, mtime = dir_stats(loc, cancel, max_entries=60_000)
p.disk_size = size
p.last_used = mtime
if progress is not None and i % 10 == 0:
progress(p.name)
progs.sort(key=lambda p: -p.best_size)
return progs
def store_apps() -> list[Program]:
"""Приложения Microsoft Store (через PowerShell, может быть медленно)."""
if sys.platform != "win32":
return []
ps = (
"Get-AppxPackage | Select-Object Name,Publisher,Version,InstallLocation | "
"ConvertTo-Csv -NoTypeInformation"
)
try:
res = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
capture_output=True, text=True, timeout=90, errors="ignore",
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
except Exception:
return []
import csv
import io
out: list[Program] = []
try:
for row in csv.DictReader(io.StringIO(res.stdout)):
loc = (row.get("InstallLocation") or "").strip()
if not loc or not os.path.isdir(loc):
continue
size, _f, _d, mtime = dir_stats(loc, max_entries=30_000)
out.append(Program(
name=row.get("Name") or "?",
publisher=(row.get("Publisher") or "").split(",")[0].replace("CN=", ""),
version=row.get("Version") or "",
install_location=loc,
disk_size=size,
last_used=mtime,
source="Store",
))
except Exception:
return out
out.sort(key=lambda p: -p.best_size)
return out
def uninstall(prog: Program) -> tuple[bool, str]:
"""Запустить штатный деинсталлятор программы (интерактивно)."""
if not prog.uninstall_cmd:
return False, "у программы нет команды удаления в реестре"
try:
subprocess.Popen(prog.uninstall_cmd, shell=True)
return True, ""
except Exception as e: # noqa: BLE001
return False, str(e)