Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions MANUAL-TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions meta/mshell.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions meta/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/api_spec.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/lua_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 },

Expand Down
2 changes: 2 additions & 0 deletions src/mshell.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
78 changes: 78 additions & 0 deletions src/settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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, &current);
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) {
Expand Down
Loading
Loading