Первый выпуск 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,156 @@
"""Тёмная и светлая темы для Dear PyGui."""
from __future__ import annotations
import dearpygui.dearpygui as dpg
from ..core.model import Risk
DARK = {
"bg": (27, 29, 34),
"bg2": (34, 37, 43),
"bg3": (42, 46, 54),
"fg": (230, 232, 236),
"fg_dim": (154, 161, 173),
"accent": (74, 144, 217),
"accent_hi": (95, 164, 232),
"ok": (62, 181, 110),
"warn": (217, 154, 43),
"danger": (217, 83, 79),
"border": (52, 57, 67),
"sel": (47, 58, 74),
}
LIGHT = {
"bg": (244, 245, 247),
"bg2": (255, 255, 255),
"bg3": (233, 235, 239),
"fg": (29, 33, 39),
"fg_dim": (95, 103, 115),
"accent": (47, 111, 181),
"accent_hi": (61, 130, 207),
"ok": (30, 122, 69),
"warn": (165, 113, 15),
"danger": (184, 51, 47),
"border": (207, 212, 220),
"sel": (214, 228, 245),
}
def risk_color(risk: Risk, palette: dict) -> tuple[int, int, int]:
return {
Risk.SAFE: palette["ok"],
Risk.LOW: palette["accent_hi"],
Risk.MEDIUM: palette["warn"],
Risk.HIGH: palette["danger"],
}[risk]
def _rgba(c: tuple[int, int, int], a: int = 255) -> tuple[int, int, int, int]:
return (c[0], c[1], c[2], a)
def build_theme(p: dict) -> int:
"""Глобальная тема под палитру ``p``."""
with dpg.theme() as theme:
with dpg.theme_component(dpg.mvAll):
col = dpg.add_theme_color
col(dpg.mvThemeCol_WindowBg, _rgba(p["bg"]))
col(dpg.mvThemeCol_ChildBg, _rgba(p["bg"]))
col(dpg.mvThemeCol_PopupBg, _rgba(p["bg2"]))
col(dpg.mvThemeCol_Border, _rgba(p["border"]))
col(dpg.mvThemeCol_BorderShadow, (0, 0, 0, 0))
col(dpg.mvThemeCol_Text, _rgba(p["fg"]))
col(dpg.mvThemeCol_TextDisabled, _rgba(p["fg_dim"]))
col(dpg.mvThemeCol_FrameBg, _rgba(p["bg3"]))
col(dpg.mvThemeCol_FrameBgHovered, _rgba(p["border"]))
col(dpg.mvThemeCol_FrameBgActive, _rgba(p["sel"]))
col(dpg.mvThemeCol_TitleBg, _rgba(p["bg2"]))
col(dpg.mvThemeCol_TitleBgActive, _rgba(p["bg2"]))
col(dpg.mvThemeCol_TitleBgCollapsed, _rgba(p["bg2"]))
col(dpg.mvThemeCol_MenuBarBg, _rgba(p["bg2"]))
col(dpg.mvThemeCol_Button, _rgba(p["bg3"]))
col(dpg.mvThemeCol_ButtonHovered, _rgba(p["border"]))
col(dpg.mvThemeCol_ButtonActive, _rgba(p["sel"]))
col(dpg.mvThemeCol_Header, _rgba(p["bg2"]))
col(dpg.mvThemeCol_HeaderHovered, _rgba(p["sel"]))
col(dpg.mvThemeCol_HeaderActive, _rgba(p["sel"]))
col(dpg.mvThemeCol_Tab, _rgba(p["bg"]))
col(dpg.mvThemeCol_TabHovered, _rgba(p["bg3"]))
col(dpg.mvThemeCol_TabActive, _rgba(p["bg2"]))
col(dpg.mvThemeCol_TabUnfocused, _rgba(p["bg"]))
col(dpg.mvThemeCol_TabUnfocusedActive, _rgba(p["bg2"]))
col(dpg.mvThemeCol_CheckMark, _rgba(p["accent_hi"]))
col(dpg.mvThemeCol_SliderGrab, _rgba(p["accent"]))
col(dpg.mvThemeCol_SliderGrabActive, _rgba(p["accent_hi"]))
col(dpg.mvThemeCol_PlotHistogram, _rgba(p["accent"]))
col(dpg.mvThemeCol_PlotHistogramHovered, _rgba(p["accent_hi"]))
col(dpg.mvThemeCol_ScrollbarBg, _rgba(p["bg"]))
col(dpg.mvThemeCol_ScrollbarGrab, _rgba(p["bg3"]))
col(dpg.mvThemeCol_ScrollbarGrabHovered, _rgba(p["border"]))
col(dpg.mvThemeCol_ScrollbarGrabActive, _rgba(p["accent"]))
col(dpg.mvThemeCol_Separator, _rgba(p["border"]))
col(dpg.mvThemeCol_TableHeaderBg, _rgba(p["bg3"]))
col(dpg.mvThemeCol_TableBorderStrong, _rgba(p["border"]))
col(dpg.mvThemeCol_TableBorderLight, _rgba(p["border"]))
col(dpg.mvThemeCol_TableRowBg, _rgba(p["bg"]))
col(dpg.mvThemeCol_TableRowBgAlt, _rgba(p["bg2"], 120))
st = dpg.add_theme_style
st(dpg.mvStyleVar_FrameRounding, 4)
st(dpg.mvStyleVar_ChildRounding, 6)
st(dpg.mvStyleVar_PopupRounding, 6)
st(dpg.mvStyleVar_WindowRounding, 6)
st(dpg.mvStyleVar_GrabRounding, 4)
st(dpg.mvStyleVar_TabRounding, 4)
st(dpg.mvStyleVar_ScrollbarRounding, 6)
st(dpg.mvStyleVar_FramePadding, 10, 6)
st(dpg.mvStyleVar_ItemSpacing, 9, 7)
st(dpg.mvStyleVar_ItemInnerSpacing, 7, 5)
st(dpg.mvStyleVar_WindowPadding, 14, 12)
st(dpg.mvStyleVar_CellPadding, 7, 5)
st(dpg.mvStyleVar_ScrollbarSize, 13)
st(dpg.mvStyleVar_WindowBorderSize, 0)
st(dpg.mvStyleVar_FrameBorderSize, 0)
return theme
def build_accent_button_theme(p: dict, base: str = "accent") -> int:
"""Тема для выделенной кнопки (основное действие / опасное действие)."""
color = p[base]
hi = tuple(min(255, v + 22) for v in color)
with dpg.theme() as theme:
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_color(dpg.mvThemeCol_Button, _rgba(color))
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, _rgba(hi))
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, _rgba(color))
dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 255, 255, 255))
dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 14, 8)
return theme
def build_card_theme(p: dict) -> int:
"""Тема для «карточки» — дочернего окна с более светлым фоном."""
with dpg.theme() as theme:
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, _rgba(p["bg2"]))
dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 8)
dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 16, 14)
return theme
def build_bar_theme(p: dict, color_key: str) -> int:
"""Тема полосы заполнения (диски, категории)."""
with dpg.theme() as theme:
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_color(dpg.mvThemeCol_PlotHistogram, _rgba(p[color_key]))
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, _rgba(p["bg3"]))
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 3)
return theme