Первый выпуск 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:
215
sysdevcleaner/gui/fonts.py
Normal file
215
sysdevcleaner/gui/fonts.py
Normal 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 рядом с приложением, "
|
||||
"иначе русский текст будет отображаться квадратами."
|
||||
)
|
||||
Reference in New Issue
Block a user