250 lines
7.4 KiB
Nix
250 lines
7.4 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.custom.power-profiles;
|
|
|
|
# ----- Hypridle configurations per profile ----------------------------------
|
|
# Each profile is a separate hypridle config file. The script swaps which one
|
|
# is active via a symlink and restarts the user service.
|
|
|
|
caffeinatedConfig = pkgs.writeText "hypridle-caffeinated.conf" ''
|
|
general {
|
|
lock_cmd = pidof hyprlock || hyprlock
|
|
before_sleep_cmd = loginctl lock-session
|
|
after_sleep_cmd = hyprctl dispatch dpms on
|
|
}
|
|
# No listeners: screen never blanks, never locks, never suspends.
|
|
'';
|
|
|
|
headlessConfig = pkgs.writeText "hypridle-headless.conf" ''
|
|
general {
|
|
lock_cmd = pidof hyprlock || hyprlock
|
|
before_sleep_cmd = loginctl lock-session
|
|
after_sleep_cmd = hyprctl dispatch dpms on
|
|
}
|
|
|
|
listener {
|
|
timeout = 10
|
|
on-timeout = loginctl lock-session
|
|
}
|
|
|
|
listener {
|
|
timeout = 15
|
|
on-timeout = hyprctl dispatch dpms off
|
|
on-resume = hyprctl dispatch dpms on
|
|
}
|
|
# No suspend listener.
|
|
'';
|
|
|
|
normalConfig = pkgs.writeText "hypridle-normal.conf" ''
|
|
general {
|
|
lock_cmd = pidof hyprlock || hyprlock
|
|
before_sleep_cmd = loginctl lock-session
|
|
after_sleep_cmd = hyprctl dispatch dpms on
|
|
}
|
|
|
|
listener {
|
|
timeout = 600
|
|
on-timeout = loginctl lock-session
|
|
}
|
|
|
|
listener {
|
|
timeout = 660
|
|
on-timeout = hyprctl dispatch dpms off
|
|
on-resume = hyprctl dispatch dpms on
|
|
}
|
|
|
|
listener {
|
|
timeout = 1800
|
|
on-timeout = systemctl suspend
|
|
}
|
|
'';
|
|
|
|
# ----- The control script ----------------------------------------------------
|
|
# Implements `next`, `prev`, `<profile>`, and `status` subcommands.
|
|
# Reads/writes the state file, swaps the hypridle config symlink, restarts
|
|
# the systemd service, and signals waybar to refresh its module.
|
|
|
|
powerProfileScript = pkgs.writeShellScriptBin "power-profile" ''
|
|
set -euo pipefail
|
|
|
|
STATE_FILE="''${XDG_STATE_HOME:-$HOME/.local/state}/power-profile"
|
|
ACTIVE_LINK="''${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hypridle-active.conf"
|
|
|
|
PROFILES_NORMAL="normal"
|
|
PROFILES_CAFFE="caffeinated"
|
|
PROFILES_HEAD="headless"
|
|
|
|
CAFFE_PATH=${caffeinatedConfig}
|
|
HEAD_PATH=${headlessConfig}
|
|
NORMAL_PATH=${normalConfig}
|
|
|
|
# Read current profile (default to normal).
|
|
current="normal"
|
|
if [ -f "$STATE_FILE" ]; then
|
|
current=$(cat "$STATE_FILE" || true)
|
|
case "$current" in
|
|
normal|caffeinated|headless) ;;
|
|
*) current="normal" ;;
|
|
esac
|
|
fi
|
|
|
|
cmd="''${1:-status}"
|
|
new=""
|
|
|
|
case "$cmd" in
|
|
normal|caffeinated|headless)
|
|
new="$cmd"
|
|
;;
|
|
next)
|
|
case "$current" in
|
|
normal) new="caffeinated" ;;
|
|
caffeinated) new="headless" ;;
|
|
headless) new="normal" ;;
|
|
*) new="normal" ;;
|
|
esac
|
|
;;
|
|
prev)
|
|
case "$current" in
|
|
normal) new="headless" ;;
|
|
headless) new="caffeinated" ;;
|
|
caffeinated) new="normal" ;;
|
|
*) new="normal" ;;
|
|
esac
|
|
;;
|
|
status)
|
|
echo "$current"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Usage: power-profile {next|prev|normal|caffeinated|headless|status}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Persist new state.
|
|
mkdir -p "$(dirname "$STATE_FILE")"
|
|
printf '%s\n' "$new" > "$STATE_FILE"
|
|
|
|
# Swap the active hypridle config symlink.
|
|
mkdir -p "$(dirname "$ACTIVE_LINK")"
|
|
case "$new" in
|
|
caffeinated) target="$CAFFE_PATH" ;;
|
|
headless) target="$HEAD_PATH" ;;
|
|
*) target="$NORMAL_PATH" ;;
|
|
esac
|
|
ln -sfn "$target" "$ACTIVE_LINK"
|
|
|
|
# Restart hypridle (best-effort; may not be running on first invocation).
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl --user restart power-profile-hypridle.service 2>/dev/null \
|
|
|| echo "power-profile: warning, could not restart hypridle service" >&2
|
|
fi
|
|
|
|
# Refresh the waybar module (signal 8).
|
|
pkill -RTMIN+8 waybar 2>/dev/null || true
|
|
|
|
echo "$new"
|
|
'';
|
|
|
|
# ----- Waybar exec script ----------------------------------------------------
|
|
# Reads the state file and emits the JSON consumed by the custom waybar module.
|
|
|
|
waybarScript = pkgs.writeShellScriptBin "power-profile-waybar" ''
|
|
state_file="''${XDG_STATE_HOME:-$HOME/.local/state}/power-profile"
|
|
profile="normal"
|
|
if [ -f "$state_file" ]; then
|
|
profile=$(cat "$state_file" || true)
|
|
case "$profile" in
|
|
normal|caffeinated|headless) ;;
|
|
*) profile="normal" ;;
|
|
esac
|
|
fi
|
|
|
|
case "$profile" in
|
|
caffeinated)
|
|
text=""
|
|
tooltip="Caffeinated — display and system stay awake"
|
|
cls="caffeinated"
|
|
;;
|
|
headless)
|
|
text=""
|
|
tooltip="Headless — screen blanks/locks, no suspend"
|
|
cls="headless"
|
|
;;
|
|
*)
|
|
text=""
|
|
tooltip="Normal — default idle behavior"
|
|
cls="normal"
|
|
;;
|
|
esac
|
|
|
|
printf '{"text":"%s","tooltip":"%s","class":"%s","alt":"%s"}\n' \
|
|
"$text" "$tooltip" "power-profile-$cls" "power-profile-$cls"
|
|
'';
|
|
|
|
in
|
|
{
|
|
options.custom.power-profiles = {
|
|
enable = lib.mkEnableOption "power profile management (normal / caffeinated / headless) via hypridle";
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# The script and the waybar helper are both installed on the user's PATH.
|
|
home.packages = [
|
|
powerProfileScript
|
|
waybarScript
|
|
pkgs.hypridle
|
|
];
|
|
|
|
# We replace the default hypridle.service with our own service that is
|
|
# driven by the active-profile symlink. Disable the standalone module
|
|
# to avoid two hypridle processes fighting over the IPC socket.
|
|
custom.hypridle.enable = lib.mkForce false;
|
|
|
|
# The systemd user service that runs hypridle with the active config.
|
|
# Started on graphical-session.target so it follows Hyprland's lifecycle.
|
|
systemd.user.services.power-profile-hypridle = {
|
|
Unit = {
|
|
Description = "Hypridle using the active power-profile config";
|
|
PartOf = [ config.wayland.systemd.target ];
|
|
After = [ "graphical-session.target" ];
|
|
ConditionEnvironment = "WAYLAND_DISPLAY";
|
|
};
|
|
Service = {
|
|
Type = "simple";
|
|
ExecStart = "${pkgs.hypridle}/bin/hypridle -c %h/.config/hypr/hypridle-active.conf";
|
|
Restart = "on-failure";
|
|
RestartSec = 3;
|
|
};
|
|
Install.WantedBy = [ config.wayland.systemd.target ];
|
|
};
|
|
|
|
# Activation: create the XDG state dir, seed the state file, and set the
|
|
# initial active-config symlink to whichever profile is in the state file
|
|
# (defaulting to "normal"). Idempotent so it's safe on every rebuild.
|
|
home.activation.powerProfileSetup = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
|
run mkdir -p $HOME/.local/state
|
|
run mkdir -p $HOME/.config/hypr
|
|
|
|
if [ ! -s $HOME/.local/state/power-profile ]; then
|
|
run ${pkgs.coreutils}/bin/install -D -m 644 /dev/null $HOME/.local/state/power-profile
|
|
run echo normal > $HOME/.local/state/power-profile
|
|
fi
|
|
|
|
current=$(${pkgs.coreutils}/bin/cat $HOME/.local/state/power-profile 2>/dev/null || echo normal)
|
|
case "$current" in
|
|
caffeinated) target=${caffeinatedConfig} ;;
|
|
headless) target=${headlessConfig} ;;
|
|
*) target=${normalConfig} ;;
|
|
esac
|
|
run ${pkgs.coreutils}/bin/ln -sfn "$target" $HOME/.config/hypr/hypridle-active.conf
|
|
'';
|
|
};
|
|
}
|