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

216 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Шрифты для 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 рядом с приложением, "
"иначе русский текст будет отображаться квадратами."
)