Files
HeagBoKaT b01cf37e66 Первый выпуск 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>
2026-09-11 14:55:00 +03:00

173 lines
5.8 KiB
Python

"""Базовые структуры данных SysDevCleaner."""
from __future__ import annotations
import os
import time
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Iterable
class Risk(IntEnum):
"""Насколько опасно удалять находку."""
SAFE = 0 # мусор в чистом виде, пересоздаётся сам
LOW = 1 # кеш: удаление вызовет лишь замедление первого запуска
MEDIUM = 2 # потеря состояния/настроек/сессий, нужна пересборка
HIGH = 3 # можно сломать проект или систему — только вручную
@property
def label(self) -> str:
return {
Risk.SAFE: "Безопасно",
Risk.LOW: "Низкий риск",
Risk.MEDIUM: "Средний риск",
Risk.HIGH: "Высокий риск",
}[self]
@property
def color(self) -> str:
return {
Risk.SAFE: "#2e9e5b",
Risk.LOW: "#4a90d9",
Risk.MEDIUM: "#d99a2b",
Risk.HIGH: "#d9534f",
}[self]
class Category(IntEnum):
WINDOWS = 0
BROWSERS = 1
DEV_CACHE = 2
DEV_PROJECT = 3
APPS = 4
LOGS = 5
PACKAGE_MANAGERS = 6
BIG_FILES = 7
DUPLICATES = 8
PROGRAMS = 9
@property
def label(self) -> str:
return {
Category.WINDOWS: "Система Windows",
Category.BROWSERS: "Браузеры",
Category.DEV_CACHE: "Кеши разработки",
Category.DEV_PROJECT: "Артефакты проектов",
Category.APPS: "Приложения",
Category.LOGS: "Логи и отчёты",
Category.PACKAGE_MANAGERS: "Пакетные менеджеры",
Category.BIG_FILES: "Крупные файлы",
Category.DUPLICATES: "Дубликаты",
Category.PROGRAMS: "Установленные программы",
}[self]
@property
def icon(self) -> str:
return {
Category.WINDOWS: "🪟",
Category.BROWSERS: "🌐",
Category.DEV_CACHE: "⚙",
Category.DEV_PROJECT: "📦",
Category.APPS: "🎮",
Category.LOGS: "📄",
Category.PACKAGE_MANAGERS: "🧰",
Category.BIG_FILES: "🐘",
Category.DUPLICATES: "👯",
Category.PROGRAMS: "🗂",
}[self]
@dataclass
class Finding:
"""Одна находка: путь (или группа путей), который можно удалить."""
path: str
title: str # человекочитаемое имя
category: Category
risk: Risk
size: int = 0 # байты
files: int = 0
dirs: int = 0
mtime: float = 0.0 # последнее изменение (unix)
detail: str = "" # подсказка что это и чем грозит удаление
group: str = "" # имя правила/группы для сводки
is_dir: bool = True
delete_contents_only: bool = False # чистим содержимое, сам каталог оставляем
members: list[str] = field(default_factory=list) # для дубликатов/мульти-путей
selected: bool = False
extra: dict = field(default_factory=dict)
@property
def age_days(self) -> float:
if not self.mtime:
return 0.0
return max(0.0, (time.time() - self.mtime) / 86400.0)
@property
def key(self) -> str:
return os.path.normcase(os.path.abspath(self.path))
@dataclass
class Program:
"""Установленная программа из реестра."""
name: str
publisher: str = ""
version: str = ""
install_date: str = ""
install_location: str = ""
uninstall_cmd: str = ""
registry_size: int = 0 # EstimatedSize из реестра
disk_size: int = 0 # реально посчитанный размер
last_used: float = 0.0 # mtime самого свежего файла
source: str = "" # HKLM / HKCU / WOW64
@property
def best_size(self) -> int:
return self.disk_size or self.registry_size
@property
def idle_days(self) -> float:
if not self.last_used:
return -1.0
return max(0.0, (time.time() - self.last_used) / 86400.0)
@dataclass
class Recommendation:
title: str
body: str
gain: int = 0 # сколько освободится, байт
severity: int = 1 # 0 инфо, 1 совет, 2 важно
findings: list[Finding] = field(default_factory=list)
@dataclass
class ScanResult:
findings: list[Finding] = field(default_factory=list)
programs: list[Program] = field(default_factory=list)
recommendations: list[Recommendation] = field(default_factory=list)
disks: list[dict] = field(default_factory=list)
errors: list[str] = field(default_factory=list)
started: float = 0.0
finished: float = 0.0
scanned_paths: int = 0
@property
def duration(self) -> float:
return max(0.0, (self.finished or time.time()) - self.started)
def total(self, cats: Iterable[Category] | None = None) -> int:
cs = set(cats) if cats else None
return sum(f.size for f in self.findings if cs is None or f.category in cs)
def by_category(self) -> dict[Category, list[Finding]]:
out: dict[Category, list[Finding]] = {}
for f in self.findings:
out.setdefault(f.category, []).append(f)
for lst in out.values():
lst.sort(key=lambda x: -x.size)
return out