Skip to content

Commit 0367ca2

Browse files
committed
feat: power manager module, refactor ghaf-powercontrol
add new power-manager module to override default logind power management: - implement suspend signal interception and handling - implement shutdown/reboot signal interception and handling - add configurable service activation through nix options - use ghaf-powercontrol for power state management ghaf-powercontrol changes: - refactor to handle any wayland compositor - fix premature exit during suspend if wlopm fails Signed-off-by: Kajus Naujokaitis <kajus.naujokaitis@unikie.com>
1 parent b7cd260 commit 0367ca2

File tree

3 files changed

+172
-9
lines changed

3 files changed

+172
-9
lines changed

modules/desktop/graphics/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
./ewwbar.nix
99
./fonts.nix
1010
./login-manager.nix
11+
./power-manager.nix
1112
./boot.nix
1213
];
1314
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright 2022-2025 TII (SSRC) and the Ghaf contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
# This module overrides logind power management using ghaf-powercontrol.
4+
# The specific operations overriden are:
5+
# - Suspend
6+
# - Shutdown
7+
# - Reboot
8+
{
9+
lib,
10+
pkgs,
11+
config,
12+
...
13+
}:
14+
let
15+
cfg = config.ghaf.graphics.power-manager;
16+
inherit (lib)
17+
mkIf
18+
mkOption
19+
types
20+
;
21+
22+
ghaf-powercontrol = pkgs.ghaf-powercontrol.override { ghafConfig = config.ghaf; };
23+
24+
logindSuspendListener = pkgs.writeShellApplication {
25+
# This listener monitors systemd's suspend signals and delays the suspend process
26+
# While the process is delayed, we use ghaf-powercontrol to handle the suspend operation instead
27+
# TODO: Investigate if suspension can be cancelled entirely if caught. Some notes on that:
28+
# - `--mode=block` does not work, as it will block suspension entirely and no signal will be sent
29+
# - `--mode=delay` works, but the system will still suspend after the delay
30+
name = "logind-suspend-listener";
31+
runtimeInputs = [
32+
pkgs.dbus
33+
pkgs.systemd
34+
pkgs.toybox
35+
ghaf-powercontrol
36+
];
37+
text = ''
38+
systemd-inhibit --what=sleep --who="ghaf-powercontrol" \
39+
--why="Handling ghaf suspend" --mode=delay \
40+
dbus-monitor --system "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'" | \
41+
while read -r line; do
42+
if echo "$line" | grep -q "boolean true"; then
43+
echo "Found prepare for sleep signal"
44+
echo "Suspending via ghaf-powercontrol"
45+
ghaf-powercontrol suspend
46+
fi
47+
done
48+
'';
49+
};
50+
51+
logindShutdownListener = pkgs.writeShellApplication {
52+
# This listener monitors systemd's shutdown/reboot signals and delays the process
53+
# While the process is delayed, we use ghaf-powercontrol to handle the shutdown/reboot operation instead
54+
name = "logind-shutdown-listener";
55+
runtimeInputs = [
56+
pkgs.dbus
57+
pkgs.systemd
58+
ghaf-powercontrol
59+
];
60+
text = ''
61+
systemd-inhibit --what=shutdown --who="ghaf-powercontrol" \
62+
--why="Handling system shutdown/reboot" --mode=delay \
63+
dbus-monitor --system "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForShutdownWithMetadata'" | \
64+
while read -r line; do
65+
if echo "$line" | grep -q "boolean true"; then
66+
echo "Found prepare for shutdown signal. Checking type..."
67+
while read -r subline; do
68+
if echo "$subline" | grep -q "reboot"; then
69+
echo "Found type: reboot"
70+
echo "Rebooting via ghaf-powercontrol"
71+
ghaf-powercontrol reboot
72+
elif echo "$subline" | grep -q "poweroff"; then
73+
echo "Found type: power-off"
74+
echo "Powering off via ghaf-powercontrol"
75+
ghaf-powercontrol poweroff
76+
fi
77+
done
78+
fi
79+
done
80+
'';
81+
};
82+
in
83+
{
84+
options.ghaf.graphics.power-manager = {
85+
enable = mkOption {
86+
description = ''
87+
Override logind power management using ghaf-powercontrol
88+
'';
89+
type = types.bool;
90+
default = false;
91+
};
92+
enableSuspendListener = mkOption {
93+
description = ''
94+
Enable the suspend signal listener service
95+
'';
96+
type = types.bool;
97+
default = true;
98+
};
99+
100+
enableShutdownListener = mkOption {
101+
description = ''
102+
Enable the shutdown/reboot signal listener service
103+
'';
104+
type = types.bool;
105+
default = true;
106+
};
107+
};
108+
109+
config = mkIf cfg.enable {
110+
systemd.services = {
111+
logind-shutdown-listener = mkIf cfg.enableShutdownListener {
112+
enable = true;
113+
description = "Ghaf logind shutdown listener";
114+
serviceConfig = {
115+
Type = "simple";
116+
Restart = "always";
117+
RestartSec = "5";
118+
ExecStart = "${lib.getExe logindShutdownListener}";
119+
};
120+
partOf = [ "graphical.target" ];
121+
wantedBy = [ "graphical.target" ];
122+
};
123+
124+
logind-suspend-listener = mkIf cfg.enableSuspendListener {
125+
# Currently system continues to suspend even after ghaf-powercontrol suspend is called
126+
# If running on host:
127+
# - Might result in two suspension requests in a row
128+
# If running on VM:
129+
# - Will result in `ghaf-powercontrol suspend` first. If woken up see next point
130+
# - Will result in a non-functional VM suspend, which gets cancelled almost immediately
131+
# - End result - System suspends, wakes up, attempts to suspend again but wakes up immediately
132+
# NOTE:
133+
# As a system service, this will run as root
134+
# This means ghaf-powercontrol will run as root and therefore fail to
135+
# find and turn off the display as part of the suspension procedures
136+
enable = true;
137+
description = "Ghaf logind suspend listener";
138+
serviceConfig = {
139+
Type = "simple";
140+
Restart = "always";
141+
RestartSec = "5";
142+
ExecStart = "${lib.getExe logindSuspendListener}";
143+
};
144+
partOf = [ "graphical.target" ];
145+
wantedBy = [ "graphical.target" ];
146+
};
147+
};
148+
};
149+
}

packages/ghaf-powercontrol/package.nix

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,12 @@
1313
}:
1414
let
1515
useGivc = ghafConfig.givc.enable;
16-
# Handle Wayland display power state
17-
waylandDisplayCmd = command: ''
18-
WAYLAND_DISPLAY=/run/user/${builtins.toString ghafConfig.users.loginUser.uid}/wayland-0 \
19-
wlopm --${command} '*'
20-
'';
2116
in
2217
writeShellApplication {
2318
name = "ghaf-powercontrol";
2419

20+
bashOptions = [ ];
21+
2522
runtimeInputs = [
2623
systemd
2724
wlopm
@@ -50,6 +47,23 @@ writeShellApplication {
5047
exit 0
5148
fi
5249
50+
try_wlopm() {
51+
local cmd="$1"
52+
local uid=${toString ghafConfig.users.loginUser.uid}
53+
54+
# Try each wayland-N socket
55+
for i in {0..9}; do
56+
export WAYLAND_DISPLAY="/run/user/$uid/wayland-$i"
57+
if [ -e "$WAYLAND_DISPLAY" ]; then
58+
if wlopm "$cmd" 2>/dev/null; then
59+
return 0
60+
fi
61+
fi
62+
done
63+
64+
return 1
65+
}
66+
5367
case "$1" in
5468
reboot|poweroff)
5569
${if useGivc then "givc-cli ${ghafConfig.givc.cliArgs}" else "systemctl"} "$1"
@@ -61,15 +75,14 @@ writeShellApplication {
6175
# Lock sessions
6276
loginctl lock-session
6377
64-
# Switch off display before suspension
65-
${waylandDisplayCmd "off"}
78+
try_wlopm "--off '*'" || true
6679
6780
# Send suspend command to host, ensure screen is on in case of failure
6881
${if useGivc then "givc-cli ${ghafConfig.givc.cliArgs}" else "systemctl"} suspend \
69-
|| ${waylandDisplayCmd "on"}
82+
|| try_wlopm "--on '*'" || true
7083
7184
# Switch on display on wakeup
72-
${waylandDisplayCmd "on"}
85+
try_wlopm "--on '*'" || true
7386
''
7487
else
7588
''

0 commit comments

Comments
 (0)