Первый выпуск 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:
144
sysdevcleaner/winui.py
Normal file
144
sysdevcleaner/winui.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Работа с нативным окном Windows и повышение прав.
|
||||
|
||||
Здесь две вещи, которые Dear PyGui сам не умеет:
|
||||
|
||||
* **Заголовок окна.** ``dpg.create_viewport(title=...)`` отдаёт строку в ANSI-функцию
|
||||
Windows как UTF-8 байты, поэтому русский заголовок превращается в
|
||||
«SysDevCleaner вЂ" анализ...». Лечится переустановкой заголовка через
|
||||
``SetWindowTextW`` — она принимает UTF-16 и показывает текст правильно.
|
||||
* **Права администратора.** Без них недоступны ``Windows\\Temp``, кеш обновлений,
|
||||
Prefetch, ``Windows.old`` и ещё несколько системных каталогов.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
IS_WIN = sys.platform == "win32"
|
||||
|
||||
# Служебный ASCII-заголовок: ставим его в create_viewport, чтобы гарантированно
|
||||
# найти окно по имени, а потом заменяем на настоящий.
|
||||
BOOT_TITLE = "SysDevCleaner (starting)"
|
||||
|
||||
# Переменная окружения-предохранитель от бесконечного перезапуска.
|
||||
NO_ELEVATE_ENV = "SYSDEVCLEAN_NO_ELEVATE"
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
"""Запущены ли мы из собранного PyInstaller exe."""
|
||||
return bool(getattr(sys, "frozen", False))
|
||||
|
||||
|
||||
def icon_path() -> str:
|
||||
"""Путь к sysdevcleaner.ico: сначала внутри сборки, потом рядом с проектом."""
|
||||
names = ("sysdevcleaner.ico",)
|
||||
roots = []
|
||||
meipass = getattr(sys, "_MEIPASS", "")
|
||||
if meipass:
|
||||
roots.append(meipass)
|
||||
roots.append(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
roots.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
roots.append(os.getcwd())
|
||||
for root in roots:
|
||||
for name in names:
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return ""
|
||||
|
||||
|
||||
def is_admin() -> bool:
|
||||
if not IS_WIN:
|
||||
return hasattr(os, "geteuid") and os.geteuid() == 0
|
||||
try:
|
||||
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ------------------------------------------------------------- заголовок ----
|
||||
|
||||
|
||||
def set_window_title(text: str, find_by: str = BOOT_TITLE) -> bool:
|
||||
"""Поставить окну настоящий заголовок через Unicode-API.
|
||||
|
||||
Возвращает True, если окно нашлось и заголовок применён.
|
||||
"""
|
||||
if not IS_WIN:
|
||||
return False
|
||||
try:
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = user32.FindWindowW(None, find_by)
|
||||
if not hwnd:
|
||||
return False
|
||||
user32.SetWindowTextW(hwnd, text)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ----------------------------------------------------- повышение прав -------
|
||||
|
||||
|
||||
def _python_launcher() -> str:
|
||||
"""Для графического режима предпочитаем pythonw.exe — без окна консоли."""
|
||||
exe = sys.executable
|
||||
if not exe:
|
||||
return ""
|
||||
folder, name = os.path.split(exe)
|
||||
if name.lower() == "python.exe":
|
||||
alt = os.path.join(folder, "pythonw.exe")
|
||||
if os.path.isfile(alt):
|
||||
return alt
|
||||
return exe
|
||||
|
||||
|
||||
def _quote(text: str) -> str:
|
||||
return f'"{text}"' if " " in text and not text.startswith('"') else text
|
||||
|
||||
|
||||
def relaunch_as_admin(extra_args: list[str] | None = None) -> bool:
|
||||
"""Перезапустить приложение с правами администратора.
|
||||
|
||||
True — повышенная копия запущена, текущий процесс должен завершиться;
|
||||
False — повышать не нужно или не получилось (пользователь нажал «Нет»),
|
||||
работаем дальше с обычными правами.
|
||||
"""
|
||||
if not IS_WIN or is_admin():
|
||||
return False
|
||||
if os.environ.get(NO_ELEVATE_ENV):
|
||||
return False
|
||||
|
||||
args = list(extra_args if extra_args is not None else sys.argv[1:])
|
||||
|
||||
if is_frozen():
|
||||
target = sys.executable
|
||||
params = " ".join(_quote(a) for a in args)
|
||||
workdir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
target = _python_launcher()
|
||||
if not target:
|
||||
return False
|
||||
# каталог, в котором лежит пакет sysdevcleaner — оттуда работает `-m sysdevcleaner`
|
||||
workdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
params = " ".join(["-m", "sysdevcleaner", *(_quote(a) for a in args)])
|
||||
|
||||
try:
|
||||
# ShellExecuteW с глаголом "runas" показывает запрос UAC
|
||||
rc = ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", target, params, workdir, 1)
|
||||
except Exception:
|
||||
return False
|
||||
# всё, что больше 32, — успех; иначе обычно ошибка 5 (пользователь отказался)
|
||||
return int(rc) > 32
|
||||
|
||||
|
||||
def ensure_admin() -> bool:
|
||||
"""Вызвать перед запуском интерфейса.
|
||||
|
||||
True — нужно завершить текущий процесс: работать продолжит повышенная копия.
|
||||
"""
|
||||
if is_admin():
|
||||
return False
|
||||
return relaunch_as_admin()
|
||||
Reference in New Issue
Block a user