Первый выпуск 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

215
sysdevcleaner/gui/fonts.py Normal file
View File

@@ -0,0 +1,215 @@
"""Шрифты для Dear PyGui с поддержкой кириллицы.
Почему без этого модуля интерфейс будет в квадратах
---------------------------------------------------
Dear PyGui использует Dear ImGui, а тот по умолчанию берёт встроенный шрифт
ProggyClean, в котором кириллицы нет вообще. Любой русский текст превращается в
пустые прямоугольники. Лечится это регистрацией собственного TTF.
Порядок вызовов важен:
1. ``dpg.create_context()``;
2. ``dpg.font_registry()`` → ``dpg.font(путь, размер)`` — регистрируем TTF,
в котором кириллица есть;
3. ``dpg.bind_font(...)`` — внутри блока реестра, пока шрифт заведомо жив;
4. и только потом ``dpg.setup_dearpygui()`` — на этом шаге собирается атлас
глифов, после него менять состав символов уже поздно.
Про диапазоны символов
----------------------
В Dear PyGui **1.x** нужно было явно просить нужные блоки Unicode::
dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) # латиница, цифры
dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) # кириллица
Начиная с **2.0** эти вызовы объявлены устаревшими и ничего не делают:
диапазоны определяются автоматически по содержимому шрифта
(«This call is a no-op because character ranges are now automatic»).
Мы всё равно их вызываем — чтобы модуль работал и на 1.x, — но глушим
предупреждения, чтобы не засорять вывод на 2.x.
Проверено на Dear PyGui 2.3.1 + Segoe UI: кириллица, «кавычки», ·, ×, –, …
отображаются корректно. А вот ⚠ и ✓ есть не во всех системных шрифтах
(в Consolas их нет), поэтому в интерфейсе они не используются.
"""
from __future__ import annotations
import os
import warnings
import dearpygui.dearpygui as dpg
# Шрифты в порядке предпочтения. Первым идёт JetBrains Mono Nerd Font: в нём
# есть и полная кириллица, и иконочные глифы Nerd Font, так что интерфейсу
# не нужен отдельный иконочный шрифт.
CANDIDATES = (
"JetBrainsMonoNerdFont-Regular.ttf", # иконки натуральной ширины
"JetBrainsMonoNLNerdFont-Regular.ttf", # без лигатур
"JetBrainsMonoNerdFontPropo-Regular.ttf",
"JetBrainsMonoNerdFontMono-Regular.ttf",
"JetBrainsMono-Regular.ttf", # обычный JetBrains Mono — без иконок
"segoeui.ttf", # системные запасные варианты
"calibri.ttf",
"tahoma.ttf",
"arial.ttf",
)
BOLD_CANDIDATES = (
"JetBrainsMonoNerdFont-Bold.ttf",
"JetBrainsMonoNLNerdFont-Bold.ttf",
"JetBrainsMonoNerdFontPropo-Bold.ttf",
"JetBrainsMonoNerdFontMono-Bold.ttf",
"JetBrainsMono-Bold.ttf",
"segoeuib.ttf",
"calibrib.ttf",
"arialbd.ttf",
)
MONO_CANDIDATES = (
"JetBrainsMonoNerdFontMono-Regular.ttf", # иконки в одну ячейку
"JetBrainsMonoNerdFont-Regular.ttf",
"JetBrainsMono-Regular.ttf",
"consola.ttf",
"cour.ttf",
)
def _font_dirs() -> list[str]:
"""Где искать TTF: вложенные в сборку, рядом с программой, системные."""
import sys
here = os.path.dirname(os.path.abspath(__file__))
project = os.path.dirname(os.path.dirname(here))
windir = os.environ.get("WINDIR", r"C:\Windows")
local = os.environ.get("LOCALAPPDATA", "")
dirs = []
# шрифты, вшитые в exe: PyInstaller распаковывает их во временный _MEIPASS
meipass = getattr(sys, "_MEIPASS", "")
if meipass:
dirs.append(os.path.join(meipass, "fonts"))
# рядом с самим exe — можно подложить свой шрифт без пересборки
dirs.append(os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "fonts"))
dirs += [
os.path.join(project, "fonts"),
os.path.join(os.getcwd(), "fonts"),
os.path.join(windir, "Fonts"),
]
if local:
dirs.append(os.path.join(local, "Microsoft", "Windows", "Fonts"))
seen, out = set(), []
for d in dirs:
key = os.path.normcase(d)
if key not in seen and os.path.isdir(d):
seen.add(key)
out.append(d)
return out
def find_font(candidates: tuple[str, ...] = CANDIDATES) -> str | None:
"""Первый существующий TTF из списка кандидатов."""
for directory in _font_dirs():
for name in candidates:
path = os.path.join(directory, name)
if os.path.isfile(path):
return path
# запасной вариант — любой .ttf из папки fonts, положенной рядом с программой
for directory in _font_dirs():
if not directory.lower().endswith("fonts") or "windows" in directory.lower():
continue
try:
for entry in sorted(os.listdir(directory)):
if entry.lower().endswith((".ttf", ".otf")):
return os.path.join(directory, entry)
except OSError:
continue
return None
def _add_ranges() -> None:
"""Диапазоны символов. На Dear PyGui 2.x — no-op, нужны только для 1.x."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
try:
dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) # латиница, цифры
dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) # кириллица
except Exception:
pass
try:
# знаки вне базовых блоков, которые встречаются в подписях
dpg.add_font_chars([0x00AB, 0x00BB, 0x00B7, 0x00D7, 0x00A0,
0x2013, 0x2014, 0x2026, 0x2192, 0x2007])
except Exception:
pass
try:
# зона частного использования — иконки Nerd Font.
# Берём только BMP: Material Design Icons лежат выше U+FFFF,
# а Dear ImGui собран с 16-битным ImWchar и их не отрисует.
dpg.add_font_range(0xE000, 0xF8FF)
except Exception:
pass
class Fonts:
"""Набор шрифтов приложения. Строить строго до ``setup_dearpygui()``."""
def __init__(self, base_size: int = 17) -> None:
self.base_size = base_size
self.path = find_font()
self.bold_path = find_font(BOLD_CANDIDATES) or self.path
self.mono_path = find_font(MONO_CANDIDATES) or self.path
self.default = None
self.small = None
self.h1 = None
self.h2 = None
self.big = None
self.mono = None
@property
def ok(self) -> bool:
return self.path is not None
@property
def has_icons(self) -> bool:
"""Найден ли Nerd Font — от этого зависит, показывать ли иконки."""
return bool(self.path) and "nerdfont" in os.path.basename(self.path).lower()
def build(self) -> None:
"""Зарегистрировать шрифты и применить основной ко всему интерфейсу."""
if not self.path:
return # кириллицы не будет, но приложение запустится
s = self.base_size
with dpg.font_registry():
with dpg.font(self.path, s) as self.default:
_add_ranges()
with dpg.font(self.path, max(12, s - 3)) as self.small:
_add_ranges()
with dpg.font(self.bold_path, s + 10) as self.h1:
_add_ranges()
with dpg.font(self.bold_path, s + 2) as self.h2:
_add_ranges()
with dpg.font(self.bold_path, s + 16) as self.big:
_add_ranges()
with dpg.font(self.mono_path, max(12, s - 3)) as self.mono:
_add_ranges()
# bind_font — внутри блока реестра: шрифт уже создан и ещё «в области видимости»
dpg.bind_font(self.default)
@staticmethod
def apply(item, font) -> None:
"""Назначить отдельному элементу другой шрифт (заголовок, крупная цифра)."""
if font is None or item is None:
return
try:
dpg.bind_item_font(item, font)
except Exception:
pass
MISSING_FONT_MESSAGE = (
"Не найден TTF-шрифт с кириллицей. Положите любой такой шрифт "
"(например, Roboto-Regular.ttf) в папку fonts рядом с приложением, "
"иначе русский текст будет отображаться квадратами."
)