# CLAUDE.md Гайд для Claude Code (claude.ai/code) и любых ИИ-ассистентов, работающих с этим репозиторием. Кратко: во что мы верим, как здесь принято писать код, что важно, а что — нет. ## Что это **XiControl** — трей-утилита для ноутбуков Xiaomi/Redmi (Windows 10/11 x64): защита заряда батареи с выбором порога (+ разовый заряд «В дорогу»), режимы производительности, OSD, переназначаемые Mi-кнопка и «мёртвые» клавиши (включая мультимедиа и калькулятор), авто-герцовка (по питанию + удержание частоты), запоминание и лимит яркости по питанию, «режим совы», тачпад и сенсорный экран вкл/выкл, мёртвая зона у нижнего края тачпада, виджет «Монитор», окно настроек в стиле Win11, проверка обновлений (оповещение, без самообновления), опциональный HTTP API для управления из локальной сети (телефон/Home Assistant). Всё управление железом — через штатный WMI-интерфейс прошивки `MiCommonInterface` (протокол MIFS, ODM Bitland). То, что делается чистым Win32 (частота экрана, яркость, тачпад, электропитание), делается чистым Win32. C# / .NET 8 / WinForms, x64, манифест `requireAdministrator`, GPLv3. ## Философия проекта (это не обсуждается) Главный принцип — **driver-free**. Утилита обходится тем, что даёт сама система: WMI-интерфейс прошивки и обычный Win32. **Никаких kernel-драйверов, WinRing0, прямого доступа к EC или записи MSR.** Это не техническое ограничение, а осознанный выбор, и он определяет, что можно, а что нет: - **Доверие и безопасность.** Пользователь ставит трей-утилиту, а не кольцо-0 в ядро. Ничего, что может уронить систему или стать вектором атаки. - **Переносимость.** Один exe, работает без установки драйверов, переживает обновления Windows. - **Честность про возможности.** Если фичу нельзя сделать driver-free — мы её **не делаем** и честно пишем почему (см. `docs/05-open-questions.md` и раздел «🚫 Не берём» в `ROADMAP.md`). Уже закрытые так вопросы: лимит TDP в ваттах, NFC-фичи. Не предлагай их снова через драйвер — это против цели. (Два вопроса из этого списка позже **переоткрылись** — урок: прежде чем закрывать как «невозможно», проверь все WMI-провайдеры и понаблюдай за OEM-софтом. 1) Мониторинг температур — сделан driver-free через Intel DPTF `root\wmi\EsifDeviceInformation`. 2) **Порог заряда** раньше считали «фиксирован 80%, бинарно» — оказалось `MIFS 0x10/0x02` многоуровневый: driver-free доступны **40/50/60/70/80/100%** (2026-07-29, см. `docs/12-charge-levels.md`). Произвольный ЛЮБОЙ % — по-прежнему нет, только дискретный набор, который прошивка валидирует.) Из этого же — **не усложнять**. Одно трей-приложение, без службы, без named pipe, без protobuf, без вторых рантаймов. Если задачу решает 20 строк Win32 — не тащим фреймворк. Набор функций прошивки **зависит от модели**. Всё определяется в рантайме; на неподдерживаемой команде (`OUT[1] == 0xE0`) фича молча выключается, приложение не падает. Лучше меньше, но надёжно. ## Как здесь принято кодить - **Пиши как вокруг.** Код плотный, лаконичный, с комментариями по-русски, объясняющими *почему* (не *что*). Подражай соседнему файлу: те же идиомы, та же плотность комментариев, тот же нейминг. - **Любая строка для пользователя — через `Loc.T(...)`** и сразу на трёх языках (RU/EN/ZH), ключ добавляется в `src/Localization/lang/{ru,en,zh}.json` (все три, порядок ключей совпадает). Хардкод текста в UI недопустим. - **Деградируй мягко.** Оборачивай железо-зависимые вызовы, лови исключения, пиши в лог (`Log.Ex`/`Log.Write`) и продолжай. Пользователь на несовместимом железе должен получить рабочее меню, а не краш. - **UI-поток не блокируем.** Долгие WMI-вызовы и смену видеорежима — в воркер (`Task.Run`). У видимой панели живёт глобальный хук мыши: если UI-поток зависнет, Windows молча снимет хук. - **Гарды для того, что прошивка сбрасывает.** Прошивка теряет лимит заряда после сна/смены питания; частота экрана — тоже. Паттерн `ChargeGuard`/`RefreshRateGuard`: подписка на события питания + дебаунс + переустановка. Новые «залипающие» настройки делай так же. - **Планировщик/реестр — только по явному действию пользователя.** Не трогаем `schtasks` на каждое сохранение конфига; SSD и системные настройки бережём. - **Проверяй сборкой и тестами.** `dotnet build XiControl.sln` должен быть 0 ошибок / 0 предупреждений (включён `TreatWarningsAsErrors` — любое предупреждение роняет сборку; осознанные исключения из правил анализаторов задокументированы в `.editorconfig`). `dotnet test XiControl.sln` — все зелёные; чистую логику покрывай тестом (образцы — в `tests/XiControl.Tests`, фейки в `Fakes.cs`), UI и железо — глазами. Иконки — глазами (см. ниже). Группируй изменения, не перезапускай приложение по мелочи. - **`dotnet format` не гонять по `OsdForm.cs`** — там намеренное колоночное выравнивание (маппинг иконок), формат его схлопывает. (`Loc.cs` больше не в этом списке: строки уехали в JSON, сам файл — обычный загрузчик.) Внешние PR (в т.ч. от других вайб-кодеров с нейронками) — приветствуются, но проходят через тот же фильтр: driver-free, не усложняем, локализация на месте, собирается чисто. Ветки ребейзим на `main`, чтобы история оставалась линейной. **Перед созданием PR подтяни свежий `main` в свой форк** (кнопка «Sync fork» → Update branch, или `git fetch upstream && git reset --hard upstream/main && git push --force origin main`) и делай фичу в **отдельной ветке** от него, а не в форк-`main`. Здесь `main` часто уходит вперёд (на каждый пуш собирается скользящий pre-release + мёрджатся другие PR), поэтому PR со старой базы приходится ребейзить при слиянии — синк это убирает. ## Команды Приложение обычно уже запущено — перед сборкой его нужно убить, иначе MSB3027 (exe заблокирован). Сборка идёт через sln (src + tests + tools); exe после неё лежит в **`src/bin/x64/Release/`** (прямой `dotnet build src/XiControl.csproj` собирает в другой каталог — не путать): ```powershell Stop-Process -Name XiControl -Force -ErrorAction SilentlyContinue dotnet build XiControl.sln -c Release # 0 ошибок / 0 предупреждений dotnet test XiControl.sln -c Release --no-build # все тесты зелёные Start-Process src/bin/x64/Release/net8.0-windows/XiControl.exe ``` Запуск показывает UAC — пользователь должен подтвердить. «Операция была отменена пользователем» из `Start-Process` — это отказ в UAC, не ошибка кода. Один переносимый exe: ```powershell dotnet publish src/XiControl.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true ``` Проверка иконок (всегда глазами до сборки приложения — результат смотреть Read'ом): ```powershell dotnet run --project tools/IconPreview -- user # сетка всех иконок → reference/user-icons-preview.png dotnet run --project tools/IconPreview -- one [size] # одна иконка → reference/one-icon.png dotnet run --project tools/IconPreview -- ico # пересобрать src/app.ico из settings.svg dotnet run --project tools/IconPreview -- svg # лист иконок в SVG + его рендер для сверки dotnet run --project tools/IconPreview -- bench # стоимость кадра анимации (GDI+, мкс) ``` Диагностика в рантайме: ошибки — в `%APPDATA%\XiControl\log.txt`, конфиг — `%APPDATA%\XiControl\config.json`. ## Архитектура Одно трей-приложение, **без службы** (проверено: admin-процесса достаточно и для SET/GET, и для WMI-событий). `Program.cs`: single-instance mutex → DI-контейнер (`Microsoft.Extensions.DependencyInjection`, все синглтоны; провайдер владеет Dispose в обратном порядке создания — компоненты инжектированное не диспоузят) → `TrayApp.Start()` → `Application.Run()`. Швы-интерфейсы (`IMifsClient`, `IConfigStore`, `IKeyEventSource`, `IPowerEvents`, `IDisplayEvents`, `IAppTimer`, `ILocalizer`) существуют ради тестов на фейках и задела под другие модели; `IAppTimer` в DI **не регистрируется** — каждому потребителю свой экземпляр (опциональный ctor-параметр). `IPowerEvents` и `IDisplayEvents` — два узких шва на одной реализации `SystemEventsSource`: в DI это один синглтон под двумя интерфейсами (окно-маршалер внутри нужно ровно одно), поэтому его `Dispose` идемпотентен — провайдер видит экземпляр дважды. - `src/Wmi/` — протокол MIFS. `Mifs.cs` — все константы: метод `MiInterface` принимает 32-байтовый буфер (`[1]` = GET `0xFA` / SET `0xFB`, `[3]` = команда, `[4]/[6]` = аргументы), статус в `OUT[1]` (`0x80` = ок, `0xE0` = не поддерживается). Команды: `0x08` режимы, `0x0A` микрофон, `0x10` заряд. `IMifsClient`/`MifsClient` — семантические Get/Set (опкоды наружу не торчат), `IKeyEventSource`/`MifsEventWatcher` — подписка на WMI-событие `HID_EVENT20` (коды клавиш — тоже в `Mifs.cs` и `docs/07`). - `src/Input/` — жесты и маршрутизация клавиш: `MiButtonGesture` (клик / двойной / удержание ~400 мс), `KeyRouter` (код клавиши → действие из конфига per-slot; исполнители — колбэки). - `src/Ui/AppController.cs` — **командный слой**: все Set*/Toggle* (заряд, «в дорогу», режимы, стратегии старта, профили, герцовка, сова, автозапуск, язык, тачпад/экран) + `Startup`/ `Shutdown`. Меню, панель, роутер и настройки зовут одни и те же методы. Команда сначала спрашивает прошивку: отказ → конфиг не трогается, зовётся `FirmwareFailed` (честный error-OSD). Результаты сообщаются именованными колбэками («что случилось»); что показать — решает TrayApp. - `src/Ui/TrayApp.cs` — тонкий монтажник: NotifyIcon, подписки на системные события, связывание колбэков контроллера с OSD/панелью/значком, first-run toast, живой тултип. Меню трея — `TrayMenuBuilder`; политика обновления значка — `TrayIconController` (кэш «без изменений — не трогаем», редкий опрос, `Polled` для тултипа); наблюдение «в дорогу» — `TravelChargeMonitor` (SystemIntegration). - `src/Ui/` остальное: `QuickPanelForm` (панель по удержанию Mi / клику по трею — чистый view над контроллером, навигация с клавиатуры; ширина фиксированная — при скрытых режимах растягиваются ячейки), `OsdForm` (всплывашки), `MonitorForm` (виджет Вт/CPU/GPU/RAM/°C), `FlyoutForm` + `FlyoutPalette` (общая база флайаутов: borderless tool-window, Region, Esc; палитра — единственный источник тёмных цветов флайаутов), `FormChrome` (DWM-тёмный заголовок + WM_SETREDRAW), `ModeUi` (режим → ключ локализации / вид OSD / акцент), `UiNav` (чистая арифметика навигации: порядок обхода ячеек панели, циклический фокус, клэмп вкладки — вынесена из форм ради юнит-тестов, XIC-9), `SettingsForm` (хост окна настроек: хром, навигация, пересборка на каждый показ — а также на смену темы, DPI и разрешения экрана: шрифты и `Sc()` снимаются в момент постройки), `SettingsActions` (сумка колбэков в контроллер; поля `required` — забытый mount ловится компилятором, CS9035), `ToggleSwitch` (рисованный Win11-тумблер), `ScaledFonts` (**шрифты только отсюда** — пиксельные под DeviceDpi, иначе после смены разрешения текст расходится с геометрией Sc), `SvgIcons` (рендер встроенных SVG через Svg.NET + кэш битмапов; `RenderByHeight` — для неквадратных картинок `assets/svg/ui/`), `FlyoutTip` (всплывающая подсказка флайаутов), `Draw` (общие примитивы), `TrayIcons`, `DarkMenu` (тёмное меню). - `src/Ui/Settings/` — начинка окна настроек: `SettingsToolkit` (фабрика виджетов: карточки, тумблеры, комбо; раздаёт `AccessibleName`), `SettingsTheme` (палитра под системную тему), `NavStrip` (левая навигация, доступна с клавиатуры), вкладки-контролы `GeneralTab` / `FeaturesTab` (доступность фич: сова/тачпад/тачскрин/`RefreshRateFeature`) / `BatteryTab` / `DisplayTab` (яркость: лимит + запоминание, и частота; видна всегда — `RefreshRateFeature=false` скрывает только раздел частоты, XIC-29) / `TouchpadTab` (поведение панели: мёртвая зона снизу — в отличие от «Функций», где только видимость) / `PerfTab` / `KeysTab` / `ApiTab` (HTTP API: тумблеры, порт, токен, пер-командные разрешения) / `AboutTab` (собирают себя в ctor). - `src/SystemIntegration/` — `ChargeGuard` (переустанавливает лимит заряда после сна/смены питания И перед уходом в сон/shutdown — EC теряет его на переходах), `RefreshRateGuard`/ `RefreshRate` (авто-герцовка, чистый `ChangeDisplaySettingsEx` по ВСТРОЕННОЙ панели — она ищется через CCD `QueryDisplayConfig`, а не `null`, иначе правился бы основной экран (XIC-21); с `HoldRefreshRate` гард слушает ещё и смену режима экрана — «удерживать частоту» после чужих изменений, только событие, без опроса), `PowerProfileGuard` (режим по питанию + независимое запоминание яркости; единственный подписчик `BrightnessWatcher` — классифицирует событие «наша запись/человек» по меткам `Brightness.Own` и раздаёт запоминанию и лимиту), `BrightnessCapGuard` (лимит яркости XIC-29: схождение половинками раз в минуту, пауза 2 ч после повторного подъёма, стоп при адаптивной яркости; целиком на потоках пула, таймеры — `WorkerTimer`), `TravelChargeMonitor` (ожидание 100%), `IPowerEvents`+`IDisplayEvents`/`SystemEventsSource` (события питания и экрана за швами, одно скрытое окно-маршалер), `IAppTimer`/`UiTimer`/ `WorkerTimer` (пул-таймер для логики вне UI-потока: WinForms-таймер, стартованный с потока WMI-событий, не тикает никогда), `Brightness` (WMI ACPI-подсветка: `Get`/`Apply`/`Ramp`-плавный ход; `Own` — метки своих записей по TTL, НЕ одноразовые — WMI-события дублируются; `AdaptiveBrightness` — детект ADAPTBRIGHT через powrprof.dll), `TouchpadControl`/`TouchscreenControl` (вкл/выкл через SetupAPI/CfgMgr32 — отключается родительский узел I2C HID, без PERSIST; общая механика в базовом `HidNodeToggle`, разница лишь в HID-коллекции: тачпад U:0005, экран U:0004; `HidNodeToggle.Restart` — перезапуск узла, чтобы драйвер перечитал настройки без перезахода в сеанс), `TouchpadDeadZone` (мёртвая зона у нижнего края: штатная curtain-зона PTP `SuperCurtainBottom` в HKLM, himetric = мм × 100; гасит НАЧАЛО касания, нажатие в зоне проходит — XIC-24), `UpdateCheck` (оповещение о новой версии: один GET к GitHub `/releases/latest` — эндпоинт сам отсекает скользящий pre-release; не чаще раза в сутки, тумблер = выключатель трафика, самообновления нет намеренно — запущенный exe не перезаписать, XIC-20), `AutoStart` (задача планировщика с SID в имени — своя на каждого пользователя, задача старого образца `XiControl` мигрирует при следующем переключении; самопочинка на старте чинит и пропавший путь, и устаревшую версию exe, дев-сборки `0.0.0` в сверке не участвуют), `AwakeMode` («режим совы»: всегда `ES_SYSTEM_REQUIRED` + крышка на AC, экран держится `ES_DISPLAY_REQUIRED` — кроме `OwlIgnoreDisplay: true` в config.json, тогда только «не спать»), `MicControl`, `KeyActions`, `Sound` (WAV-джинглы), `BatteryInfo`, `PowerDraw`, `GpuTelemetry` (загрузка/ватты/частота iGPU через **Intel IGCL** — user-mode API драйвера Intel, `ControlLib.dll` из System32; driver-free, без админа, ленивая инициализация; не Intel → ряд GPU в «Мониторе» просто не появляется, см. `docs/09`); **HTTP API (XIC-13, opt-in)** — `HttpApi` (хост на `HttpListener`/http.sys, без ASP.NET Core; создаётся только при включённой фиче → выключено = 0 CPU), `ApiRouter` (авторизация Bearer+SHA-256 constant-time и белый список маршрутов — тестируется на фейках), `ApiSettings`/`ApiSettingsStore` (настройки в `%ProgramData%\XiControl\api.json` под ACL «запись только Administrators/SYSTEM» + проверка владельца при чтении — правкой config.json API не включить), `ApiFirewall` (правило netsh `LocalSubnet` только по явному LAN-тумблеру). Команды идут в тот же `AppController`, маршалятся в UI-поток; bind `127.0.0.1` по умолчанию, LAN — отдельным тумблером. - `src/Config/` — `AppConfig` (POCO: config.json + миграции в `MigrateKeyActions`; `Save()` остался на объекте, но persistence — за `IConfigStore`/`JsonConfigStore`; `LegacyLanguageConverter` — миграция старого формата языка); `src/Localization/` — переводы лежат в `lang/{ru,en,zh}.json` (встроены как ресурсы), `Loc.cs` — только загрузчик и `Loc.T` (+ тонкий шов `ILocalizer`); `src/Log.cs` — журнал (`Log.Write`/`Log.Ex`). - `tests/XiControl.Tests` — юнит-тесты (xUnit + FluentAssertions 6.x, x64) чистой логики: протокол, миграции конфига, guard-ы, жесты, роутер, контроллер, «в дорогу», значок трея. Всё железо/таймеры — фейки из `Fakes.cs`; реестр/schtasks/смену видеорежима юнитами не трогаем. CI (`.github/workflows/ci.yml`): build + test — жёсткие гейты, format — мягкий. Документация протокола — в `docs/`: **01-wmi-protocol.md — главный документ** (транспорт, буфер, команды, события), 02 — каталог функций, 05 — открытые/закрытые вопросы, 07 — карта клавиш, 08 — спецификация иконок, 09 — мониторинг питания. ## Иконки - SVG рисует **сам пользователь**; Claude только интегрирует и правит технически. `assets/svg/osd/` — цветные 128×128, `assets/svg/tray/` — монохром 24×24 (`currentColor`, перекрашивается под тему), `assets/svg/ui/` — неквадратные картинки интерфейса (кнопка Buy Me a Coffee 545×153): рисуются через `SvgIcons.RenderByHeight` — обычный `Render` строго квадратный и растянул бы их. - **Фирменная палитра — [docs/10-colors.md](docs/10-colors.md)** (Material-набор): бери цвета оттуда, не вводи произвольные оттенки. Пары вкл/выкл: «выкл» = синие → Blue Grey, ripple убрать, рука `#FFCC80`. - SVG встраиваются в exe как EmbeddedResource с `LogicalName svg.<имя>.svg` — glob берёт **все** подпапки `assets/svg/**` под плоским именем, поэтому **имена файлов уникальны по всем трём каталогам (osd/, tray/, ui/)**, дубликат роняет сборку. - **Svg.NET (пакет Svg 3.4.7) молча игнорирует `` и ``** — элемент рисуется без выреза, без ошибки. Любые «дырки» — только геометрией: один `` с `fill-rule="evenodd"`. - Трей: контуры в 16px разваливаются — только заливка-силуэты; рендер строго в фактический `GetSystemMetrics(SM_CXSMICON)`, системный даунскейл размывает. ## Чего избегать (см. docs/03-architecture.md) - Kernel-драйверы / WinRing0 / MSR — против цели проекта (см. «Философия»). - Отдельная служба, пайпы, protobuf — переусложнение. - `schtasks`/PowerShell на каждое сохранение конфига — планировщик трогать только по явному действию. - Синхронные WMI-вызовы с длинными таймаутами в UI-потоке — выносить в воркер. - Возвращать закрытые driver-free-вопросы (TDP, произвольный % заряда, NFC) через драйвер. ## Релизы и CI - `git tag v0.X.Y && git push` → GitHub Release с двумя exe (self-contained + framework-dependent), плюс триггерит winget-workflow. - Тег с суффиксом через дефис (`v0.7.0-pre`) помечается pre-release и winget **не** трогает. - Каждый push в `main` собирает скользящий pre-release под тегом `pre` — всегда свежий билд из main; winget его не видит (слушает только `release: [released]`). - **SonarCloud** (`.github/workflows/sonar.yml`, только push в `main`): бесплатный тариф даёт лишь встроенный Quality Gate «Sonar way» — свой не завести (Team/Enterprise), а он требует **≥80% покрытия нового кода**. Поэтому из ИЗМЕРЕНИЯ покрытия (`sonar.coverage.exclusions`) исключены края, которые мы намеренно не покрываем юнитами: `src/Ui/**`, `src/SystemIntegration/**`, `Program.cs`, `tools/**` и живой WMI — `MifsClient.cs`/`MifsEventWatcher.cs`. Чистая логика (`Mifs.cs`, конфиг, guard-ы, роутер) измеряется и должна оставаться покрытой. Добавляешь новый файл с живым железом — впиши его в исключения, иначе гейт покраснеет на ровном месте. - Локальная сборка помечается версией `0.0.0-dev` (дев-дефолт в `XiControl.csproj`, суффикс виден в AboutTab); реальную версию подставляет CI из тега: `publish -p:Version=X.Y.Z`. - winget-PR Komac пересобирает из exe и теряет рукописные поля (`MinimumOSVersion` — в 0.7.0 и 0.8.0); `winget.yml` возвращает их сам шагом `.github/scripts/winget-restore-fields.ps1` (правит ветку форка, идемпотентен). После релиза всё равно свериться с эталоном `reference/winget/<версия>/`. ## Ограничения среды - Железо-специфично: разработка и проверка — на Xiaomi Book Pro 14 (TM2424). WMI-вызовы к прошивке нельзя проверить без этого железа; сборку — можно всегда. - Порог «беречь батарею» — дискретный набор уровней прошивки (на TM2424: 40/50/60/70/80/100%), произвольный процент невозможен; прошивка сама валидирует набор (см. `docs/12-charge-levels.md`). - Датчик тока батареи беззнаковый (величина, не направление) — детали в `docs/09-power-monitoring.md`.