feat(desktop): add hyprlock and hypridle (task 0026)
Add a session-lock screen and idle management to the Hyprland-native subdirectory, enabled through the desktop aggregator. hyprlock draws through the compositor session-lock protocol, themed by Stylix, with a centered field and clock. Super+X locks directly through a guarded launch so the key works without the idle daemon. hypridle locks on idle and powers the displays off, with tunable timeouts, and locks before every suspend. Lid-close routes through logind to suspend, which the pre-sleep lock covers, so the lid lands at a locked screen.
This commit was merged in pull request #18.
This commit is contained in:
@@ -14,8 +14,33 @@ Bind lock on `Super+X`.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] hyprlock and hypridle modules exist in the Hyprland-native subdirectory and are enabled by the aggregator.
|
- [x] hyprlock and hypridle modules exist in the Hyprland-native subdirectory and are enabled by the aggregator.
|
||||||
- [ ] hyprlock uses the compositor session-lock protocol.
|
- [x] hyprlock uses the compositor session-lock protocol.
|
||||||
- [ ] hypridle triggers lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
- [x] hypridle triggers lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
||||||
- [ ] Lock is bound on `Super+X`.
|
- [x] Lock is bound on `Super+X`.
|
||||||
- [ ] neogaia builds green under `nix flake check`.
|
- [x] neogaia builds green under `nix flake check`.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **hyprlock is inherently the session-lock client.**
|
||||||
|
Criterion 2 needs no option: hyprlock draws its surface through the ext-session-lock protocol, so the compositor owns the surface and it survives a crash of the locker.
|
||||||
|
The module therefore carries only geometry and behaviour.
|
||||||
|
|
||||||
|
- **Stylix themes the lock screen.**
|
||||||
|
Colors and the lock background come from Stylix's hyprlock target, which merges into the same `background` and `input-field` blocks, so the module sets only field geometry and a `$TIME` label.
|
||||||
|
|
||||||
|
- **lid-close is wired through logind, not a hypridle listener.**
|
||||||
|
hypridle cannot observe lid events, so the module sets `services.logind.settings.Login.HandleLidSwitch = "suspend"`, and the shared `before_sleep_cmd` locks ahead of the suspend.
|
||||||
|
The lid therefore lands at a locked screen, satisfying the criterion by outcome even though the trigger is logind's.
|
||||||
|
|
||||||
|
- **`Super+X` is self-contained.**
|
||||||
|
The keybind execs a guarded hyprlock launch directly (`pidof hyprlock || hyprlock`) rather than `loginctl lock-session`, so the lock key works whenever hyprlock is enabled, without depending on hypridle being the running lock handler.
|
||||||
|
hypridle's own idle and suspend paths still funnel through `loginctl lock-session` so logind tracks the locked state on those paths.
|
||||||
|
|
||||||
|
- **Idle-suspend was left out.**
|
||||||
|
The spec enumerates lock-on-idle, screen-off, lock-before-suspend, and lid-close, so hypridle does not itself suspend on idle.
|
||||||
|
`before_sleep_cmd` handles lock-before-suspend for the lid and any manual or externally configured suspend.
|
||||||
|
Adding an idle-suspend stage is a reasonable future knob but was not requested here.
|
||||||
|
|
||||||
|
- **One hyprlock package.**
|
||||||
|
Both the keybind and hypridle's `lock_cmd` reference `programs.hyprlock.package`, so the locker never splits versions between the two call sites.
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ in
|
|||||||
# any one of them while the single flag above enables the whole desktop.
|
# any one of them while the single flag above enables the whole desktop.
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.desktop.hyprland.enable = lib.mkDefault true;
|
modules.desktop.hyprland.enable = lib.mkDefault true;
|
||||||
|
modules.desktop.hyprlock.enable = lib.mkDefault true;
|
||||||
|
modules.desktop.hypridle.enable = lib.mkDefault true;
|
||||||
modules.desktop.login.enable = lib.mkDefault true;
|
modules.desktop.login.enable = lib.mkDefault true;
|
||||||
modules.desktop.mako.enable = lib.mkDefault true;
|
modules.desktop.mako.enable = lib.mkDefault true;
|
||||||
modules.desktop.rofi.enable = lib.mkDefault true;
|
modules.desktop.rofi.enable = lib.mkDefault true;
|
||||||
|
|||||||
69
modules/desktop/hyprland/hypridle.nix
Normal file
69
modules/desktop/hyprland/hypridle.nix
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# Idle management: hypridle locks on idle, powers the displays off, and locks
|
||||||
|
# before every suspend, so an unattended session always lands at hyprlock.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.hypridle;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
|
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
||||||
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
|
|
||||||
|
# The guard drops the call when a locker is already up, so no idle trigger
|
||||||
|
# stacks a second hyprlock over the first.
|
||||||
|
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.hypridle = {
|
||||||
|
enable = lib.mkEnableOption "hypridle idle management";
|
||||||
|
|
||||||
|
lockTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 300;
|
||||||
|
description = "Seconds of inactivity before the screen locks.";
|
||||||
|
};
|
||||||
|
|
||||||
|
screenOffTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 360;
|
||||||
|
description = "Seconds of inactivity before the displays are powered off.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
# Closing the lid suspends, and every suspend locks first through the
|
||||||
|
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
||||||
|
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
||||||
|
|
||||||
|
home-manager.users.${user}.services.hypridle = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
general = {
|
||||||
|
lock_cmd = lockCmd;
|
||||||
|
before_sleep_cmd = "loginctl lock-session";
|
||||||
|
# Waking restores the displays the screen-off listener may have cut.
|
||||||
|
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
||||||
|
};
|
||||||
|
|
||||||
|
listener = [
|
||||||
|
# Lock on idle.
|
||||||
|
{
|
||||||
|
timeout = cfg.lockTimeout;
|
||||||
|
on-timeout = "loginctl lock-session";
|
||||||
|
}
|
||||||
|
# Power the displays off a little later, restoring them on any activity.
|
||||||
|
{
|
||||||
|
timeout = cfg.screenOffTimeout;
|
||||||
|
on-timeout = "${hyprctl} dispatch dpms off";
|
||||||
|
on-resume = "${hyprctl} dispatch dpms on";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
61
modules/desktop/hyprland/hyprlock.nix
Normal file
61
modules/desktop/hyprland/hyprlock.nix
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# The lock screen: hyprlock, a session-lock client whose surface the compositor owns, so it survives a crash of the locker rather than exposing the session.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.hyprlock;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
|
# The hyprlock this module installs, so the keybind and the idle daemon lock
|
||||||
|
# with one package and never split versions.
|
||||||
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
home-manager.users.${user} = {
|
||||||
|
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
||||||
|
# so only geometry and behaviour are set here.
|
||||||
|
programs.hyprlock = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
general = {
|
||||||
|
hide_cursor = true;
|
||||||
|
# No progress bar flashes before the field is ready to take input.
|
||||||
|
disable_loading_bar = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# A centered password field; its colors are the Stylix target's.
|
||||||
|
input-field = {
|
||||||
|
size = "260, 52";
|
||||||
|
rounding = 8;
|
||||||
|
position = "0, -100";
|
||||||
|
halign = "center";
|
||||||
|
valign = "center";
|
||||||
|
};
|
||||||
|
|
||||||
|
# The current time, above the field.
|
||||||
|
label = {
|
||||||
|
text = "$TIME";
|
||||||
|
font_size = 48;
|
||||||
|
position = "0, 120";
|
||||||
|
halign = "center";
|
||||||
|
valign = "center";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Lock on Super+X.
|
||||||
|
# The guard drops the keypress when a locker is already up, so a second
|
||||||
|
# hyprlock never stacks over the first.
|
||||||
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
|
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user