diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2c168..103577d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to mshell are documented here. This project adheres to ## Unreleased +- feat(power): configure the active power plan from `power.setup` and + `--settings` — `screen_off`, `sleep` and `hibernate` timeouts in minutes + (0 never) and `power_button`, `sleep_button` and `lid` actions, each with a + `_battery` twin. Written through Windows' documented power API, since the + Settings handlers only offer these as translated labels + ## 0.15.24 — 2026-09-30 - build: add probe_uipi under make probe diff --git a/MANUAL-TESTS.md b/MANUAL-TESTS.md index d6a6276..d79c66c 100644 --- a/MANUAL-TESTS.md +++ b/MANUAL-TESTS.md @@ -874,6 +874,25 @@ for. `mshell.exe --settings get` before you start records what you had. - Quit mshell and boot once into Explorer: Settings shows every value the config set. +## Power plan settings + +- `mshell.exe --settings get power.screen_off power.lid` matches + `powercfg /qh SCHEME_CURRENT SUB_VIDEO VIDEOIDLE` (seconds / 60) and + `powercfg /qh SCHEME_CURRENT SUB_BUTTONS LIDACTION` (0 nothing, 1 sleep, + 2 hibernate, 3 shutdown). +- `--settings set power.screen_off 4` changes only the plugged-in value: + `powercfg` shows the AC index as 240 and the DC index untouched. Set it back. +- `--settings set power.lid display_off` fails naming the four lid actions; the + power and sleep buttons accept `display_off`. +- Put `mshell.power.setup{ screen_off = 4 }` in `init.lua` and save: the log has + `settings: power.screen_off: 3 -> 4`, and the screen goes dark after four idle + minutes. Switch to another power plan and the value reverts to that plan's own + until the next reload writes it again. +- A timeout `powercfg` set to a non-whole minute (`powercfg /change + monitor-timeout-ac` only takes minutes, so use + `powercfg /setacvalueindex SCHEME_CURRENT SUB_VIDEO VIDEOIDLE 90`) reads back + as `90 seconds`, and setting it from the config writes the minute value. + ## Floating windows stay on top Open one tiled window and one floating one (`Win+f`), overlapping. diff --git a/Makefile b/Makefile index d5b2dbb..957fa8c 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,7 @@ MSHELL_SRCS = $(SRC_DIR)/main.c \ $(SRC_DIR)/settings_parse.c \ $(SRC_DIR)/settings_catalog.c \ $(SRC_DIR)/settings_sync.c \ + $(SRC_DIR)/settings_power.c \ $(SRC_DIR)/display.c \ $(SRC_DIR)/update_parse.c \ $(SRC_DIR)/tree_algebra.c \ diff --git a/README.md b/README.md index 7a4883a..d78d898 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,10 @@ tiled, driven entirely from the keyboard and configured in Lua. signing out and booting into Explorer. mshell only writes a field that differs from what Windows already has, and a field you don't mention is left alone — deleting a line stops mshell asserting it but does not undo it. Scrolling, - cursor size, key repeat, the accessibility keys, light and dark theme and Game - Mode come the same way, from `mouse.setup`, `keyboard.setup`, `theme.setup` and - `gaming.setup`. + cursor size, key repeat, the accessibility keys, light and dark theme, Game + Mode, and the power plan's screen and sleep timeouts and button and lid + actions come the same way, from `mouse.setup`, `keyboard.setup`, + `theme.setup`, `gaming.setup` and `power.setup`. - **Nothing is remembered across a restart**: every start is the one your `init.lua` describes. A layout or master ratio you change at runtime lasts as long as mshell does, and a config reload keeps it — unless a `desktop_rule` @@ -377,8 +378,8 @@ and it obeys the same rules — `no_ring` windows, fullscreen windows and rather than a string: `mshell.window.*`, `mshell.desktop.*`, `mshell.monitor.*`, `mshell.display.*`, `mshell.layout.*`, `mshell.bar.*`, `mshell.appearance.*`, `mshell.keys.*`, `mshell.mouse.*`, `mshell.keyboard.*`, `mshell.theme.*`, -`mshell.gaming.*`, `mshell.system.*`, `mshell.media.*`, `mshell.exec`, -`mshell.notify`, `mshell.log`, `mshell.config.*`, `mshell.on`. +`mshell.gaming.*`, `mshell.power.*`, `mshell.system.*`, `mshell.media.*`, +`mshell.exec`, `mshell.notify`, `mshell.log`, `mshell.config.*`, `mshell.on`. The full list is not reproduced here, because a list in a README goes stale. It is generated from the same table the binary dispatches through and shipped @@ -416,8 +417,16 @@ mshell.mouse.setup{ scroll_lines = 5, scroll_by = "lines", reverse_scroll = fals mshell.keyboard.setup{ repeat_delay = 0, repeat_rate = 31, sticky_keys_shortcut = false } mshell.theme.setup{ mode = "dark", transparency = false, animations = false } mshell.gaming.setup{ game_mode = true } +mshell.power.setup{ screen_off = 10, sleep = 0, power_button = "sleep", + lid_battery = "hibernate" } ``` +Power is the exception to the handlers: Settings only offers its timeouts as +translated labels ("15 minutes"), so `power.setup` writes the active power plan +through Windows' documented power API instead. Timeouts are minutes, 0 meaning +never; every field has a `_battery` twin for the value used off the charger. The +values belong to the plan, so switching to another plan brings its own. + They are saved to your profile exactly as the Settings page would save them, so they outlive mshell. On every start and reload mshell hands the lot to a separate `mshell.exe` in the background, which writes only the ones that differ diff --git a/meta/mshell.lua b/meta/mshell.lua index da44360..a264167 100644 --- a/meta/mshell.lua +++ b/meta/mshell.lua @@ -659,6 +659,15 @@ mshell.gaming = {} ---@param opts mshell.GamingOpts function mshell.gaming.setup(opts) end +---Windows' power plan: when the screen turns off and the PC sleeps, and what its buttons and lid do. +---@class mshell.power +mshell.power = {} + +---Configure the active power plan. Saved with the plan. +---Only while the config is loading. +---@param opts mshell.PowerOpts +function mshell.power.setup(opts) end + ---Write a line to the mshell log. ---@class mshell.log ---@overload fun(message: string) diff --git a/meta/types.lua b/meta/types.lua index b9702e0..05a946d 100644 --- a/meta/types.lua +++ b/meta/types.lua @@ -245,6 +245,23 @@ function Window:kill() end ---@field game_mode boolean? ---@field game_bar boolean? A controller's Xbox button opens Game Bar. +---@alias mshell.ButtonAction "nothing"|"sleep"|"hibernate"|"shutdown"|"display_off" +---@alias mshell.LidAction "nothing"|"sleep"|"hibernate"|"shutdown" + +---@class mshell.PowerOpts +---@field screen_off integer? Minutes idle before the screen turns off when plugged in, 0 to 1440; 0 never. +---@field screen_off_battery integer? The same on battery. +---@field sleep integer? Minutes idle before the PC sleeps when plugged in; 0 never. +---@field sleep_battery integer? The same on battery. +---@field hibernate integer? Minutes idle before the PC hibernates when plugged in; 0 never. +---@field hibernate_battery integer? The same on battery. +---@field power_button mshell.ButtonAction? When plugged in. +---@field power_button_battery mshell.ButtonAction? +---@field sleep_button mshell.ButtonAction? When plugged in. +---@field sleep_button_battery mshell.ButtonAction? +---@field lid mshell.LidAction? Closing the lid when plugged in. +---@field lid_battery mshell.LidAction? + ---@class mshell.WhichKeyOpts ---@field enabled boolean? ---@field delay integer? Milliseconds before the panel appears. diff --git a/src/api_spec.c b/src/api_spec.c index 02c6dca..9f30e56 100644 --- a/src/api_spec.c +++ b/src/api_spec.c @@ -687,6 +687,14 @@ const ApiEntry api_spec[] = { API_SETTER, API_CONFIG_ONLY, "Configure gaming. Saved to your profile.", "opts: mshell.GamingOpts", NULL }, + { "power", NULL, ACTION_NONE, + API_NAMESPACE, 0, + "Windows' power plan: when the screen turns off and the PC sleeps, and what its buttons and lid do.", + "", NULL }, + { "power.setup", NULL, ACTION_NONE, + API_SETTER, API_CONFIG_ONLY, + "Configure the active power plan. Saved with the plan.", + "opts: mshell.PowerOpts", NULL }, { "log", NULL, ACTION_NONE, API_METHOD, API_CALLABLE, "Write a line to the mshell log.", diff --git a/src/lua_api.c b/src/lua_api.c index a7b892e..11d3de6 100644 --- a/src/lua_api.c +++ b/src/lua_api.c @@ -975,6 +975,7 @@ static int setup_setting_namespace(lua_State *L, const char *ns) { static int lua_mshell_keyboard_setup(lua_State *L) { return setup_setting_namespace(L, "keyboard"); } static int lua_mshell_theme_setup(lua_State *L) { return setup_setting_namespace(L, "theme"); } static int lua_mshell_gaming_setup(lua_State *L) { return setup_setting_namespace(L, "gaming"); } +static int lua_mshell_power_setup(lua_State *L) { return setup_setting_namespace(L, "power"); } static int lua_mshell_monitor_rule(lua_State *L) { if (g.cfg.monitor_rule_count >= MAX_MONITOR_RULES) @@ -2242,6 +2243,7 @@ static const ApiImpl api_impl[] = { { "keyboard.setup", lua_mshell_keyboard_setup }, { "theme.setup", lua_mshell_theme_setup }, { "gaming.setup", lua_mshell_gaming_setup }, + { "power.setup", lua_mshell_power_setup }, { "notify", lua_mshell_notify }, { "notify.setup", lua_mshell_set_notify }, diff --git a/src/mshell.h b/src/mshell.h index 832c1e3..7887f2e 100644 --- a/src/mshell.h +++ b/src/mshell.h @@ -907,6 +907,8 @@ void tweaks_list(void); void tweaks_emit_reg(const wchar_t *group, bool undo); int settings_cli(int argc, wchar_t **argv); +DWORD settings_power_read(const SettingField *f, DWORD *value); +DWORD settings_power_write(const SettingField *f, DWORD value); void settings_sync(void); void update_check_async(void); diff --git a/src/settings.c b/src/settings.c index c78f19d..0abcd56 100644 --- a/src/settings.c +++ b/src/settings.c @@ -550,7 +550,85 @@ static void job_set(SettingJob *job, void *item) { report_line(r, L"%ls: %ls -> %ls", job->name, before, after); } +static void power_word(const SettingField *f, DWORD raw, wchar_t *out, size_t cap) { + if (f->kind == FIELD_INT && raw % (DWORD)f->power_scale != 0) { + _snwprintf(out, cap - 1, L"%lu seconds", (unsigned long)raw); + out[cap - 1] = L'\0'; + return; + } + SettingValue v; + memset(&v, 0, sizeof v); + if (f->kind == FIELD_INT) { + v.kind = SV_UINT32; + v.u = raw / (DWORD)f->power_scale; + } else { + v.kind = SV_STRING; + _snwprintf(v.s, SETTING_TEXT_MAX - 1, L"%lu", (unsigned long)raw); + } + word_of(f, &v, out, cap); +} + +static void power_job(SettingJob *job) { + Report *r = &job->report; + const SettingField *f = job->field; + + DWORD current = 0; + DWORD rc = settings_power_read(f, ¤t); + if (rc != ERROR_SUCCESS) { + report_fail(r, job->name, L"Windows would not read it from the active power plan " + L"(error %lu)", (unsigned long)rc); + return; + } + + wchar_t before[SETTING_TEXT_MAX + 32]; + power_word(f, current, before, ARRAYSIZE(before)); + if (!job->set) { + report_line(r, L"%ls = %ls", job->name, before); + return; + } + + wchar_t *end = NULL; + unsigned long n = wcstoul(job->want, &end, 10); + if (end == job->want || *end) { + report_fail(r, job->name, L"'%ls' is not a value for it", job->want); + return; + } + DWORD want = (DWORD)n * (DWORD)f->power_scale; + + wchar_t after[SETTING_TEXT_MAX + 32]; + power_word(f, want, after, ARRAYSIZE(after)); + if (current == want) { + report_line(r, L"%ls: already %ls", job->name, after); + return; + } + + rc = settings_power_write(f, want); + if (rc == ERROR_ACCESS_DENIED) { + report_fail(r, job->name, L"Windows refused the change: this power plan needs " + L"administrator rights to edit"); + return; + } + if (rc != ERROR_SUCCESS) { + report_fail(r, job->name, L"Windows refused the change (error %lu)", (unsigned long)rc); + return; + } + + DWORD now = 0; + if (settings_power_read(f, &now) == ERROR_SUCCESS && now != want) { + wchar_t now_t[SETTING_TEXT_MAX + 32]; + power_word(f, now, now_t, ARRAYSIZE(now_t)); + report_fail(r, job->name, L"Windows accepted %ls but it still reads %ls", after, now_t); + return; + } + report_line(r, L"%ls: %ls -> %ls", job->name, before, after); +} + static void job_run(SettingJob *job) { + if (settings_field_is_power(job->field)) { + power_job(job); + return; + } + wchar_t why[512]; void *item = open_item(job->field->id, why, ARRAYSIZE(why)); if (!item) { diff --git a/src/settings_catalog.c b/src/settings_catalog.c index 7dfb7b5..3130ce8 100644 --- a/src/settings_catalog.c +++ b/src/settings_catalog.c @@ -29,103 +29,161 @@ static const FieldChoice LIGHT_DARK[] = { { "dark", L"Dark" }, }; +static const FieldChoice BUTTON_ACTIONS[] = { + { "nothing", L"0" }, + { "sleep", L"1" }, + { "hibernate", L"2" }, + { "shutdown", L"3" }, + { "display_off", L"4" }, +}; + +static const FieldChoice LID_ACTIONS[] = { + { "nothing", L"0" }, + { "sleep", L"1" }, + { "hibernate", L"2" }, + { "shutdown", L"3" }, +}; + +#define SUB_VIDEO L"{7516b95f-f776-4464-8c53-06167f40cc99}" +#define SUB_SLEEP L"{238c9fa8-0aad-41ed-83f4-97be242c8f20}" +#define SUB_BUTTONS L"{4f971e89-eebd-4455-a8de-9e59040e7347}" +#define VIDEO_IDLE L"{3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e}" +#define STANDBY_IDLE L"{29f6c1db-86da-48c5-9fdb-f2b67b1f44da}" +#define HIBERNATE_IDLE L"{9d7815a6-7ee4-497e-8888-515a05f02364}" +#define POWER_BUTTON L"{7648efa3-dd9c-4e3e-b566-50f929386280}" +#define SLEEP_BUTTON L"{96996bc0-ad50-47ec-923b-6f41874dd9eb}" +#define LID_CLOSE L"{5ca83367-6e45-459f-a27b-476b1d01c936}" + +#define NO_POWER NULL, NULL, false, 0 + +#define POWER_TIMEOUT(path, battery, sub, setting, doc) \ + { path, NULL, FIELD_INT, 0, 1440, NULL, 0, doc, sub, setting, battery, 60 } + +#define POWER_ACTION(path, battery, setting, list, doc) \ + { path, NULL, FIELD_CHOICE, 0, 0, CHOICES(list), doc, SUB_BUTTONS, setting, battery, 1 } + static const SettingField CATALOG[] = { { "mouse.scroll_lines", L"SystemSettings_Input_Mouse_SetScrollLines", FIELD_INT, 1, 100, NULL, 0, - "Lines one notch of the wheel scrolls." }, + "Lines one notch of the wheel scrolls.", NO_POWER }, { "mouse.scroll_by", L"SystemSettings_Input_Mouse_SetScrollPage", FIELD_CHOICE, 0, 0, CHOICES(SCROLL_BY), - "Whether a notch scrolls lines or a whole screen." }, + "Whether a notch scrolls lines or a whole screen.", NO_POWER }, { "mouse.scroll_inactive", L"SystemSettings_Input_Mouse_WheelRouting", FIELD_BOOL, 0, 0, NULL, 0, - "Scroll the window under the pointer even when it is not focused." }, + "Scroll the window under the pointer even when it is not focused.", NO_POWER }, { "mouse.reverse_scroll", L"SystemSettings_Input_Mouse_ReverseWheelDirection", FIELD_TOGGLE, 0, 0, CHOICES(WHEEL_DIRECTION), - "Scroll the other way: rolling down moves the content up." }, + "Scroll the other way: rolling down moves the content up.", NO_POWER }, { "mouse.cursor_size", L"SystemSettings_Accessibility_MouseCursorSize", FIELD_INT, 1, 15, NULL, 0, - "Pointer size, 1 being Windows' normal size." }, + "Pointer size, 1 being Windows' normal size.", NO_POWER }, { "mouse.double_click_speed", L"SystemSettings_Accessibility_MouseDblClickSpeed", FIELD_INT, 1, 11, NULL, 0, - "How fast a double-click must be, 1 slowest to 11 fastest." }, + "How fast a double-click must be, 1 slowest to 11 fastest.", NO_POWER }, { "mouse.hide_while_typing", L"SystemSettings_Accessibility_MouseHideMouseWhileTyping", FIELD_BOOL, 0, 0, NULL, 0, - "Hide the pointer while you type." }, + "Hide the pointer while you type.", NO_POWER }, { "mouse.pointer_shadow", L"SystemSettings_Accessibility_MousePointerShadow", FIELD_BOOL, 0, 0, NULL, 0, - "Draw a shadow under the pointer." }, + "Draw a shadow under the pointer.", NO_POWER }, { "mouse.trails", L"SystemSettings_Accessibility_MouseTrails", FIELD_BOOL, 0, 0, NULL, 0, - "Leave a trail behind the pointer." }, + "Leave a trail behind the pointer.", NO_POWER }, { "mouse.trails_length", L"SystemSettings_Accessibility_MouseTrailsLength", FIELD_INT, 2, 7, NULL, 0, - "Length of that trail. Only takes effect with trails on." }, + "Length of that trail. Only takes effect with trails on.", NO_POWER }, { "mouse.locate_with_ctrl", L"SystemSettings_Accessibility_MouseSonar", FIELD_BOOL, 0, 0, NULL, 0, - "Pressing Ctrl rings the pointer so you can find it." }, + "Pressing Ctrl rings the pointer so you can find it.", NO_POWER }, { "mouse.click_lock", L"SystemSettings_Accessibility_MouseClickLock", FIELD_BOOL, 0, 0, NULL, 0, - "Holding the button briefly locks it down, so a drag needs no held button." }, + "Holding the button briefly locks it down, so a drag needs no held button.", NO_POWER }, { "mouse.snap_to_default", L"SystemSettings_Accessibility_MouseSnapToDefaultButton", FIELD_BOOL, 0, 0, NULL, 0, - "Move the pointer onto a dialog's default button when it opens." }, + "Move the pointer onto a dialog's default button when it opens.", NO_POWER }, { "keyboard.repeat_delay", L"SystemSettings_Accessibility_Keyboard_CharacterRepeatDelay", FIELD_INT, 0, 3, NULL, 0, - "Wait before a held key repeats, 0 shortest (about 250 ms) to 3 longest (about 1 s)." }, + "Wait before a held key repeats, 0 shortest (about 250 ms) to 3 longest (about 1 s).", NO_POWER }, { "keyboard.repeat_rate", L"SystemSettings_Accessibility_Keyboard_CharacterRepeatRate", FIELD_INT, 0, 31, NULL, 0, - "How fast a held key repeats, 0 slowest to 31 fastest." }, + "How fast a held key repeats, 0 slowest to 31 fastest.", NO_POWER }, { "keyboard.sticky_keys", L"SystemSettings_Accessibility_Keyboard_IsStickyKeysEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Sticky Keys: press modifiers one at a time." }, + "Sticky Keys: press modifiers one at a time.", NO_POWER }, { "keyboard.sticky_keys_shortcut", L"SystemSettings_Accessibility_Keyboard_StickyShortcutEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Pressing Shift five times turns Sticky Keys on." }, + "Pressing Shift five times turns Sticky Keys on.", NO_POWER }, { "keyboard.filter_keys", L"SystemSettings_Accessibility_Keyboard_IsFilterKeysEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Filter Keys: ignore brief or repeated keystrokes." }, + "Filter Keys: ignore brief or repeated keystrokes.", NO_POWER }, { "keyboard.filter_keys_shortcut", L"SystemSettings_Accessibility_Keyboard_FilterShortcutEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Holding right Shift for eight seconds turns Filter Keys on." }, + "Holding right Shift for eight seconds turns Filter Keys on.", NO_POWER }, { "keyboard.toggle_keys", L"SystemSettings_Accessibility_Keyboard_IsToggleKeysEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Toggle Keys: beep on Caps, Num and Scroll Lock." }, + "Toggle Keys: beep on Caps, Num and Scroll Lock.", NO_POWER }, { "keyboard.toggle_keys_shortcut", L"SystemSettings_Accessibility_Keyboard_ToggleShortcutEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Holding Num Lock for five seconds turns Toggle Keys on." }, + "Holding Num Lock for five seconds turns Toggle Keys on.", NO_POWER }, { "keyboard.underline_access_keys", L"SystemSettings_Accessibility_Keyboard_IsUnderlineShortcutEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Always underline the access key in menus and dialogs." }, + "Always underline the access key in menus and dialogs.", NO_POWER }, { "theme.mode", L"SystemSettings_Personalize_Color_ColorMode", FIELD_CHOICE, 0, 0, CHOICES(COLOR_MODE), - "Light or dark everywhere, or custom to pick apps and system separately." }, + "Light or dark everywhere, or custom to pick apps and system separately.", NO_POWER }, { "theme.apps", L"SystemSettings_Personalize_Color_AppsTheme", FIELD_CHOICE, 0, 0, CHOICES(LIGHT_DARK), - "Theme apps use when mode is custom." }, + "Theme apps use when mode is custom.", NO_POWER }, { "theme.system", L"SystemSettings_Personalize_Color_SystemTheme", FIELD_CHOICE, 0, 0, CHOICES(LIGHT_DARK), - "Theme Windows' own surfaces use when mode is custom." }, + "Theme Windows' own surfaces use when mode is custom.", NO_POWER }, { "theme.transparency", L"SystemSettings_Personalize_Color_EnableTransparency", FIELD_BOOL, 0, 0, NULL, 0, - "Translucent window and menu backgrounds." }, + "Translucent window and menu backgrounds.", NO_POWER }, { "theme.accent_title_bars", L"SystemSettings_Personalize_Color_ColorPrevalenceTitleBar", FIELD_BOOL, 0, 0, NULL, 0, - "Paint title bars and window borders in the accent colour." }, + "Paint title bars and window borders in the accent colour.", NO_POWER }, { "theme.animations", L"SystemSettings_EaseOfAccess_IsAnimationsEnabled", FIELD_BOOL, 0, 0, NULL, 0, - "Windows' own animation effects." }, + "Windows' own animation effects.", NO_POWER }, { "theme.always_show_scrollbars", L"SystemSettings_EaseOfAccess_AlwaysShowScrollbars", FIELD_BOOL, 0, 0, NULL, 0, - "Keep scrollbars visible instead of collapsing them." }, + "Keep scrollbars visible instead of collapsing them.", NO_POWER }, { "gaming.game_mode", L"SystemSettings_Gaming_GameMode_Toggle", FIELD_BOOL, 0, 0, NULL, 0, - "Game Mode: prioritise the game in the foreground." }, + "Game Mode: prioritise the game in the foreground.", NO_POWER }, { "gaming.game_bar", L"SystemSettings_Gaming_GameBar_Toggle", FIELD_BOOL, 0, 0, NULL, 0, - "Let a controller's Xbox button open Game Bar." }, + "Let a controller's Xbox button open Game Bar.", NO_POWER }, + + POWER_TIMEOUT("power.screen_off", false, SUB_VIDEO, VIDEO_IDLE, + "Minutes idle before the screen turns off when plugged in; 0 never."), + POWER_TIMEOUT("power.screen_off_battery", true, SUB_VIDEO, VIDEO_IDLE, + "Minutes idle before the screen turns off on battery; 0 never."), + POWER_TIMEOUT("power.sleep", false, SUB_SLEEP, STANDBY_IDLE, + "Minutes idle before the PC sleeps when plugged in; 0 never."), + POWER_TIMEOUT("power.sleep_battery", true, SUB_SLEEP, STANDBY_IDLE, + "Minutes idle before the PC sleeps on battery; 0 never."), + POWER_TIMEOUT("power.hibernate", false, SUB_SLEEP, HIBERNATE_IDLE, + "Minutes idle before the PC hibernates when plugged in; 0 never."), + POWER_TIMEOUT("power.hibernate_battery", true, SUB_SLEEP, HIBERNATE_IDLE, + "Minutes idle before the PC hibernates on battery; 0 never."), + POWER_ACTION("power.power_button", false, POWER_BUTTON, BUTTON_ACTIONS, + "What the power button does when plugged in."), + POWER_ACTION("power.power_button_battery", true, POWER_BUTTON, BUTTON_ACTIONS, + "What the power button does on battery."), + POWER_ACTION("power.sleep_button", false, SLEEP_BUTTON, BUTTON_ACTIONS, + "What the sleep button does when plugged in."), + POWER_ACTION("power.sleep_button_battery", true, SLEEP_BUTTON, BUTTON_ACTIONS, + "What the sleep button does on battery."), + POWER_ACTION("power.lid", false, LID_CLOSE, LID_ACTIONS, + "What closing the lid does when plugged in."), + POWER_ACTION("power.lid_battery", true, LID_CLOSE, LID_ACTIONS, + "What closing the lid does on battery."), }; #define CATALOG_COUNT ((int)(sizeof CATALOG / sizeof CATALOG[0])) @@ -156,6 +214,10 @@ bool settings_field_in_namespace(const SettingField *f, const char *ns) { return strncmp(f->path, ns, n) == 0 && f->path[n] == '.'; } +bool settings_field_is_power(const SettingField *f) { + return f->power_setting != NULL; +} + static void set_error(char *error, size_t cap, const char *fmt, ...) { if (!error || cap == 0) return; va_list ap; diff --git a/src/settings_catalog.h b/src/settings_catalog.h index ce75bea..97d7d89 100644 --- a/src/settings_catalog.h +++ b/src/settings_catalog.h @@ -27,6 +27,10 @@ typedef struct { const FieldChoice *choices; int choice_count; const char *doc; + const wchar_t *power_subgroup; + const wchar_t *power_setting; + bool power_battery; + int power_scale; } SettingField; typedef enum { @@ -52,6 +56,7 @@ const SettingField *settings_catalog_find(const char *path); const char *settings_field_leaf(const SettingField *f); bool settings_field_in_namespace(const SettingField *f, const char *ns); +bool settings_field_is_power(const SettingField *f); bool settings_field_from_input(const SettingField *f, const FieldInput *in, char *text, size_t cap, char *error, size_t error_cap); diff --git a/src/settings_power.c b/src/settings_power.c new file mode 100644 index 0000000..a164234 --- /dev/null +++ b/src/settings_power.c @@ -0,0 +1,44 @@ +#include "mshell.h" + +#include +#include + +static DWORD power_target(const SettingField *f, GUID *subgroup, GUID *setting) { + if (FAILED(IIDFromString(f->power_subgroup, subgroup)) || + FAILED(IIDFromString(f->power_setting, setting))) + return ERROR_INVALID_PARAMETER; + return ERROR_SUCCESS; +} + +DWORD settings_power_read(const SettingField *f, DWORD *value) { + GUID subgroup, setting; + DWORD rc = power_target(f, &subgroup, &setting); + if (rc != ERROR_SUCCESS) return rc; + + GUID *scheme = NULL; + rc = PowerGetActiveScheme(NULL, &scheme); + if (rc != ERROR_SUCCESS) return rc; + + rc = f->power_battery + ? PowerReadDCValueIndex(NULL, scheme, &subgroup, &setting, value) + : PowerReadACValueIndex(NULL, scheme, &subgroup, &setting, value); + LocalFree(scheme); + return rc; +} + +DWORD settings_power_write(const SettingField *f, DWORD value) { + GUID subgroup, setting; + DWORD rc = power_target(f, &subgroup, &setting); + if (rc != ERROR_SUCCESS) return rc; + + GUID *scheme = NULL; + rc = PowerGetActiveScheme(NULL, &scheme); + if (rc != ERROR_SUCCESS) return rc; + + rc = f->power_battery + ? PowerWriteDCValueIndex(NULL, scheme, &subgroup, &setting, value) + : PowerWriteACValueIndex(NULL, scheme, &subgroup, &setting, value); + if (rc == ERROR_SUCCESS) rc = PowerSetActiveScheme(NULL, scheme); + LocalFree(scheme); + return rc; +} diff --git a/test/test_settings_catalog.c b/test/test_settings_catalog.c index b9e54f1..173f762 100644 --- a/test/test_settings_catalog.c +++ b/test/test_settings_catalog.c @@ -3,6 +3,7 @@ #include #include +#include static bool is_identifier(const char *s, size_t len) { if (len == 0 || !islower((unsigned char)s[0])) return false; @@ -11,6 +12,22 @@ static bool is_identifier(const char *s, size_t len) { return true; } +static bool is_guid(const wchar_t *s) { + if (!s || wcslen(s) != 38 || s[0] != L'{' || s[37] != L'}') return false; + for (int k = 1; k < 37; k++) { + bool dash = (k == 9 || k == 14 || k == 19 || k == 24); + if (dash ? s[k] != L'-' : !iswxdigit(s[k])) return false; + } + return true; +} + +static bool is_number(const wchar_t *s) { + if (!s || !*s) return false; + for (; *s; s++) + if (!iswdigit(*s)) return false; + return true; +} + static char *read_file(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; @@ -38,15 +55,37 @@ static void test_rows_are_well_formed(void) { if (!dot) continue; CHECK(is_identifier(f->path, (size_t)(dot - f->path)), "%s: namespace is an identifier", f->path); CHECK(is_identifier(dot + 1, strlen(dot + 1)), "%s: field is an identifier", f->path); - CHECK(wcsncmp(f->id, SETTING_ID_PREFIX, wcslen(SETTING_ID_PREFIX)) == 0, - "%s: handler id carries the SystemSettings_ prefix", f->path); + if (settings_field_is_power(f)) { + CHECK(f->id == NULL, "%s: a power row has no handler id", f->path); + CHECK(is_guid(f->power_subgroup) && is_guid(f->power_setting), + "%s: power subgroup and setting are braced GUIDs", f->path); + CHECK(f->power_scale >= 1, "%s: power scale is positive", f->path); + CHECK(f->kind == FIELD_INT || f->kind == FIELD_CHOICE, + "%s: power rows are numbers or choices", f->path); + CHECK(f->kind != FIELD_CHOICE || f->power_scale == 1, + "%s: a power choice is stored as its index", f->path); + for (int c = 0; f->kind == FIELD_CHOICE && c < f->choice_count; c++) + CHECK(is_number(f->choices[c].option), "%s: option %s is an index", + f->path, f->choices[c].word); + } else { + CHECK(f->id && wcsncmp(f->id, SETTING_ID_PREFIX, wcslen(SETTING_ID_PREFIX)) == 0, + "%s: handler id carries the SystemSettings_ prefix", f->path); + CHECK(f->power_subgroup == NULL && f->power_scale == 0, + "%s: a handler row has no power target", f->path); + } CHECK(f->doc && f->doc[0], "%s: documented", f->path); CHECK(settings_catalog_find(f->path) == f, "%s: found by path", f->path); for (int j = k + 1; j < settings_catalog_count(); j++) { const SettingField *other = settings_catalog_at(j); CHECK(strcmp(f->path, other->path) != 0, "%s: path is unique", f->path); - CHECK(wcscmp(f->id, other->id) != 0, "%s and %s share a handler id", f->path, other->path); + if (f->id && other->id) + CHECK(wcscmp(f->id, other->id) != 0, "%s and %s share a handler id", + f->path, other->path); + if (settings_field_is_power(f) && settings_field_is_power(other)) + CHECK(wcscmp(f->power_setting, other->power_setting) != 0 || + f->power_battery != other->power_battery, + "%s and %s write the same power setting", f->path, other->path); } switch (f->kind) { @@ -187,6 +226,35 @@ static void test_word_of(void) { CHECK(!settings_field_word_of(lines, &raw, word, sizeof word), "bool for an int field"); } +static void test_power_rows(void) { + wchar_t out[64]; + char err[160], word[32]; + SettingValue raw; + const SettingField *screen = must("power.screen_off"); + const SettingField *battery = must("power.screen_off_battery"); + const SettingField *button = must("power.power_button"); + const SettingField *lid = must("power.lid"); + if (!screen || !battery || !button || !lid) return; + + CHECK(settings_field_is_power(screen) && !screen->power_battery, "screen_off is the plugged-in value"); + CHECK(battery->power_battery, "screen_off_battery is the battery value"); + CHECK(!settings_field_is_power(must("mouse.scroll_lines")), "handler rows are not power rows"); + + CHECK(settings_field_engine_text(screen, "10", out, 64, err, sizeof err) && !wcscmp(out, L"10"), + "a timeout is passed on in minutes"); + CHECK(!settings_field_engine_text(screen, "1441", out, 64, err, sizeof err), "a day at most"); + CHECK(settings_field_engine_text(button, "shutdown", out, 64, err, sizeof err) && !wcscmp(out, L"3"), + "an action becomes its power index"); + CHECK(!settings_field_engine_text(lid, "display_off", out, 64, err, sizeof err), + "the lid has no display_off"); + + memset(&raw, 0, sizeof raw); + raw.kind = SV_STRING; + wcscpy(raw.s, L"4"); + CHECK(settings_field_word_of(button, &raw, word, sizeof word) && !strcmp(word, "display_off"), + "a power index reads back as its word"); +} + static void test_describe(void) { char out[64]; const SettingField *lines = must("mouse.scroll_lines"); @@ -237,6 +305,7 @@ int main(void) { test_from_input(); test_engine_text(); test_word_of(); + test_power_rows(); test_describe(); test_documented_in_meta(); return tests_report("settings_catalog");