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() {