From be3bcb9ecf5f3527d49401d7914f594ab744b88c Mon Sep 17 00:00:00 2001 From: Oksion Date: Wed, 29 Jul 2026 15:27:32 +0500 Subject: [PATCH] =?UTF-8?q?feat(owl):=20=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=20O?= =?UTF-8?q?wlIgnoreDisplay=20=E2=80=94=20=D1=81=D0=BE=D0=B2=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D1=82=D1=80=D0=BE=D0=B3=D0=B0=D0=B5=D1=82=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit «Режим совы» всегда выставлял ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED, то есть вместе со сном запрещал и гашение экрана. Для основного сценария — «система не должна уснуть, пока я подключаюсь удалённо» — экран вторичен, и подсвеченная матрица часами горит впустую. Поле OwlIgnoreDisplay в config.json (правится руками, как тайминги Mi-кнопки): true → флаг ES_DISPLAY_REQUIRED не выставляется, экран гаснет по обычному таймауту схемы питания, система по-прежнему бодрствует. Дефолт false — поведение существующих установок НЕ меняется, пока поле не добавят: опция описывает отклонение, а не текущее поведение. Тест на дефолт держит этот инвариант кодом. Крышку опция не затрагивает (закрытая крышка на AC всё так же не усыпляет), медиаплееры просят удержание экрана сами — фильмы не прерываются. В UI не выносим: редкая настройка, не усложняем панель и настройки. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 8 +++++++- CLAUDE.md | 4 +++- README.md | 8 +++++--- README.ru.md | 8 +++++--- src/Config/AppConfig.cs | 14 +++++++++++++- src/SystemIntegration/AwakeMode.cs | 16 +++++++++++----- tests/XiControl.Tests/AppConfigMigrationTests.cs | 5 +++++ 7 files changed, 49 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f45e176..c417d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ ## [Unreleased] -Пока пусто — планы в [ROADMAP.md](ROADMAP.md). +### Добавлено +- **`OwlIgnoreDisplay` в config.json** — «Режим совы» умеет держать бодрой только систему, отпуская + экран: с `true` он больше не выставляет `ES_DISPLAY_REQUIRED`, и экран гаснет по обычному таймауту + схемы питания. Для сценария «комп не должен уснуть ради удалённого доступа», где подсвеченная + матрица горит впустую. Крышку опция не затрагивает — закрытая крышка на сети всё так же не + усыпляет; медиаплееры запрашивают удержание экрана сами, так что фильмы не прерываются. + По умолчанию `false` — поведение существующих установок не меняется, пока поле не добавят руками. ## [0.8.0] — 2026-07-29 diff --git a/CLAUDE.md b/CLAUDE.md index ff619bc..2e00cb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -167,7 +167,9 @@ WMI-событий). `Program.cs`: single-instance mutex → DI-контейне `Brightness` (WMI ACPI-подсветка), `TouchpadControl`/`TouchscreenControl` (вкл/выкл через SetupAPI/CfgMgr32 — отключается родительский узел I2C HID, без PERSIST; общая механика в базовом `HidNodeToggle`, разница лишь в HID-коллекции: тачпад U:0005, экран U:0004), - `AutoStart` (задача планировщика + самопочинка пути), `AwakeMode` («режим совы»), + `AutoStart` (задача планировщика + самопочинка пути), `AwakeMode` («режим совы»: всегда + `ES_SYSTEM_REQUIRED` + крышка на AC, экран держится `ES_DISPLAY_REQUIRED` — кроме + `OwlIgnoreDisplay: true` в config.json, тогда только «не спать»), `MicControl`, `KeyActions`, `Sound` (WAV-джинглы), `BatteryInfo`, `PowerDraw`; **HTTP API (XIC-13, opt-in)** — `HttpApi` (хост на `HttpListener`/http.sys, без ASP.NET Core; создаётся только при включённой фиче → выключено = 0 CPU), `ApiRouter` (авторизация Bearer+SHA-256 diff --git a/README.md b/README.md index 7ee2850..0ca5045 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,11 @@ current direction is shown by color (charging green / discharging orange).* control; a disabled feature disappears from the menu and panel entirely. - 🎨 The tray icon changes with the mode, monochrome to match a light/dark taskbar; a dark menu matching the system theme (switches on the fly). -- 🦉 **"Owl mode"** — don't turn off the display or sleep; a closed lid on AC power only turns - off the display (on battery — regular sleep). Owl in the panel / a checkbox in the menu; - power timings aren't changed, the lid action is restored afterwards. +- 🦉 **"Owl mode"** — don't sleep, and don't turn off the display *(optional)*; a closed lid on + AC power only turns off the display (on battery — regular sleep). Owl in the panel / a checkbox + in the menu; power timings aren't changed, the lid action is restored afterwards. + If you only need the machine to stay awake (for a remote session, say), set + `"OwlIgnoreDisplay": true` in config — the screen then blanks as usual instead of burning idle. The feature can be hidden entirely (`"OwlMode": false` in config). - 🖥️ **Auto refresh rate** — plug in the charger → 120 Hz, unplug → 60 Hz (rates configurable in config: `AcRefreshRate`/`BatteryRefreshRate`; if the panel lacks diff --git a/README.ru.md b/README.ru.md index 2d46036..6d35b2c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -89,9 +89,11 @@ управление частотой; выключенная функция исчезает из меню и панели целиком. - 🎨 Значок в трее меняется по режиму, монохром под светлую/тёмную панель задач; тёмное меню в тон системной теме (переключается на лету). -- 🦉 **«Режим совы»** — не гасить экран и не засыпать; закрытая крышка на питании - от сети лишь выключает экран (на батарее — штатный сон). Сова в панели / галочка - в меню; тайминги электропитания не изменяются, действие крышки восстанавливается. +- 🦉 **«Режим совы»** — не засыпать и не гасить экран *(опционально)*; закрытая крышка + на питании от сети лишь выключает экран (на батарее — штатный сон). Сова в панели / + галочка в меню; тайминги электропитания не изменяются, действие крышки восстанавливается. + Если сова нужна только чтобы система не уснула (например, ради удалённого доступа) — + `"OwlIgnoreDisplay": true` в конфиге, и экран будет гаснуть как обычно, не выгорая впустую. Можно скрыть фичу целиком (`"OwlMode": false` в конфиге). - 🖥️ **Авто-герцовка** — подключил зарядку → экран 120 Гц, отключил → 60 Гц (частоты настраиваются в конфиге: `AcRefreshRate`/`BatteryRefreshRate`; если такой diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs index 33a4bf4..09784d3 100644 --- a/src/Config/AppConfig.cs +++ b/src/Config/AppConfig.cs @@ -154,9 +154,21 @@ public sealed class AppConfig /// Частота экрана (Гц) от батареи. Настраивается только правкой config.json. public int BatteryRefreshRate { get; set; } = 60; - /// Режим «Не спать» активен: экран не гаснет, сна нет; крышка на AC — «ничего не делать». + /// Режим «Не спать» активен: сна нет; крышка на AC — «ничего не делать»; + /// экран не гаснет, если не задан . public bool Awake { get; set; } = false; + /// + /// «Сова не трогает монитор»: с true режим держит бодрой только систему, а экран гаснет по + /// обычному таймауту схемы питания (не выставляем `ES_DISPLAY_REQUIRED`). Для сценария + /// «комп не должен уснуть ради удалённого доступа» — матрица там горит впустую. + /// Дефолт false = прежнее поведение, экран удерживается: поведение существующих установок + /// не меняется, пока поле не добавят в config.json руками. Крышку опция не затрагивает — + /// закрытая крышка на сети всё так же не усыпляет. Медиаплееры просят `ES_DISPLAY_REQUIRED` + /// сами, поэтому фильмы не прерываются и с true. Применяется при следующем включении совы. + /// + public bool OwlIgnoreDisplay { get; set; } = false; + /// Исходное действие крышки (AC) до включения «Не спать» — для восстановления, в т.ч. после сбоя. public int? AwakeSavedLidAc { get; set; } diff --git a/src/SystemIntegration/AwakeMode.cs b/src/SystemIntegration/AwakeMode.cs index 5633b12..c7f4af8 100644 --- a/src/SystemIntegration/AwakeMode.cs +++ b/src/SystemIntegration/AwakeMode.cs @@ -4,10 +4,12 @@ namespace XiControl.SystemIntegration; /// -/// Режим «Не спать»: пока включён — экран не гаснет и система не засыпает -/// (SetThreadExecutionState), а закрытие крышки НА ПИТАНИИ ОТ СЕТИ не уводит -/// в сон (действие крышки AC → «ничего не делать» через Power Management API). -/// На батарее крышка не трогается — там штатный сон. +/// Режим «Не спать»: пока включён — система не засыпает (SetThreadExecutionState), +/// а закрытие крышки НА ПИТАНИИ ОТ СЕТИ не уводит в сон (действие крышки AC → +/// «ничего не делать» через Power Management API). На батарее крышка не трогается — +/// там штатный сон. Экран дополнительно удерживается негаснущим — кроме случая, когда в +/// config.json задан (дефолт false = прежнее +/// поведение); с ним сова держит бодрой только систему, для удалённого доступа. /// Исходное действие крышки хранится в config.json (восстановление после сбоя). /// public static class AwakeMode @@ -50,8 +52,12 @@ public static bool Enable(AppConfig cfg) } finally { LocalFree(pScheme); } + // ES_SYSTEM_REQUIRED — всегда, это и есть суть режима; экран отпускаем только по опции + uint flags = ES_CONTINUOUS | ES_SYSTEM_REQUIRED; + if (!cfg.OwlIgnoreDisplay) flags |= ES_DISPLAY_REQUIRED; + // возвращает прежние флаги; 0 — вызов не принят (сон/экран не удержим) - if (SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED) == 0) + if (SetThreadExecutionState(flags) == 0) { Log.Write("AwakeMode: SetThreadExecutionState не принял флаги"); return false; diff --git a/tests/XiControl.Tests/AppConfigMigrationTests.cs b/tests/XiControl.Tests/AppConfigMigrationTests.cs index 36f4b74..03116ec 100644 --- a/tests/XiControl.Tests/AppConfigMigrationTests.cs +++ b/tests/XiControl.Tests/AppConfigMigrationTests.cs @@ -24,6 +24,11 @@ public void FreshConfig_GetsDocumentedDefaults() cfg.ProjKeyAction.Should().Be("projection"); } + [Fact] + public void OwlIgnoreDisplay_DefaultsToOff_SoOwlKeepsHoldingTheScreen() + => new AppConfig().OwlIgnoreDisplay.Should().BeFalse( + "поле только для тех, кто добавит его руками; без него сова ведёт себя как раньше"); + [Fact] public void LegacyChargeFirst_MapsClickToChargeAndDoubleToModes() {