Power Profiles
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
../../modules/home-manager/pi
|
../../modules/home-manager/pi
|
||||||
../../modules/home-manager/tmux
|
../../modules/home-manager/tmux
|
||||||
../../modules/home-manager/openspec
|
../../modules/home-manager/openspec
|
||||||
|
../../modules/home-manager/power-profiles
|
||||||
|
|
||||||
inputs.zen-browser.homeModules.beta
|
inputs.zen-browser.homeModules.beta
|
||||||
];
|
];
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
pi.enable = true;
|
pi.enable = true;
|
||||||
tmux.enable = true;
|
tmux.enable = true;
|
||||||
openspec.enable = true;
|
openspec.enable = true;
|
||||||
|
power-profiles.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home.packages =
|
home.packages =
|
||||||
|
|||||||
@@ -13,31 +13,6 @@
|
|||||||
config = lib.mkIf config.custom.hypridle.enable {
|
config = lib.mkIf config.custom.hypridle.enable {
|
||||||
services.hypridle = {
|
services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
timeout = 660;
|
|
||||||
on-timeout = "hyprctl dispatch dpms off";
|
|
||||||
on-resume = "hyprctl dispatch dpms on";
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
timeout = 1800;
|
|
||||||
on-timeout = "systemctl suspend";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ in
|
|||||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE")
|
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||||
hl.exec_cmd("waybar")
|
hl.exec_cmd("waybar")
|
||||||
hl.exec_cmd("hypridle")
|
hl.exec_cmd("systemctl --user enable --now power-profile-hypridle.service")
|
||||||
hl.exec_cmd("swaync")
|
hl.exec_cmd("swaync")
|
||||||
hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice-Right 24")
|
hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice-Right 24")
|
||||||
end)
|
end)
|
||||||
@@ -258,6 +258,10 @@ in
|
|||||||
hl.bind(mod_alt .. " + L", hl.dsp.exec_cmd("hyprlock"))
|
hl.bind(mod_alt .. " + L", hl.dsp.exec_cmd("hyprlock"))
|
||||||
hl.bind(mod .. " + ALT + CTRL + L", hl.dsp.exec_cmd("systemctl suspend"))
|
hl.bind(mod .. " + ALT + CTRL + L", hl.dsp.exec_cmd("systemctl suspend"))
|
||||||
|
|
||||||
|
-- ---------- Power profile cycling ----------
|
||||||
|
hl.bind(mod .. " + bracketright", hl.dsp.exec_cmd("power-profile next"))
|
||||||
|
hl.bind(mod .. " + bracketleft", hl.dsp.exec_cmd("power-profile prev"))
|
||||||
|
|
||||||
-- ---------- Waybar reload ----------
|
-- ---------- Waybar reload ----------
|
||||||
hl.bind(mod_alt .. " + R", hl.dsp.exec_cmd("/home/jpporta/.config/waybar/scripts/launch.sh"))
|
hl.bind(mod_alt .. " + R", hl.dsp.exec_cmd("/home/jpporta/.config/waybar/scripts/launch.sh"))
|
||||||
|
|
||||||
|
|||||||
@@ -1,162 +1,24 @@
|
|||||||
local function insert_package_json(root_files, field, fname)
|
|
||||||
local pkg_path = vim.fs.find("package.json", { upward = true, path = fname })[1]
|
|
||||||
if pkg_path then
|
|
||||||
local ok, content = pcall(vim.fn.readfile, pkg_path)
|
|
||||||
if ok then
|
|
||||||
local json = table.concat(content, "\n")
|
|
||||||
if string.find(json, '"' .. field .. '"') then
|
|
||||||
table.insert(root_files, pkg_path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return root_files
|
|
||||||
end
|
|
||||||
|
|
||||||
local function root_markers_with_field(root_files, files, field, fname)
|
|
||||||
for _, f in ipairs(files) do
|
|
||||||
local path = vim.fs.find(f, { upward = true, path = fname })[1]
|
|
||||||
if path then
|
|
||||||
local ok, content = pcall(vim.fn.readfile, path)
|
|
||||||
if ok then
|
|
||||||
local text = table.concat(content, "\n")
|
|
||||||
if string.find(text, field) then
|
|
||||||
table.insert(root_files, path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return root_files
|
|
||||||
end
|
|
||||||
---@brief
|
|
||||||
--- https://github.com/tailwindlabs/tailwindcss-intellisense
|
|
||||||
---
|
|
||||||
--- Tailwind CSS Language Server can be installed via npm:
|
|
||||||
---
|
|
||||||
--- npm install -g @tailwindcss/language-server
|
|
||||||
---@type vim.lsp.Config
|
|
||||||
return {
|
return {
|
||||||
cmd = { "tailwindcss-language-server", "--stdio" },
|
cmd = { "tailwindcss-language-server", "--stdio" },
|
||||||
-- filetypes copied and adjusted from tailwindcss-intellisense
|
|
||||||
filetypes = {
|
filetypes = {
|
||||||
-- html
|
"html", "css", "scss", "javascript", "javascriptreact",
|
||||||
"aspnetcorerazor",
|
"typescript", "typescriptreact", "svelte", "vue", "astro",
|
||||||
"astro",
|
},
|
||||||
"astro-markdown",
|
root_markers = {
|
||||||
"blade",
|
"tailwind.config.js", "tailwind.config.cjs", "tailwind.config.ts",
|
||||||
"clojure",
|
"postcss.config.js", "package.json", ".git",
|
||||||
"django-html",
|
|
||||||
"htmldjango",
|
|
||||||
"edge",
|
|
||||||
"eelixir", -- vim ft
|
|
||||||
"elixir",
|
|
||||||
"ejs",
|
|
||||||
"erb",
|
|
||||||
"eruby", -- vim ft
|
|
||||||
"gohtml",
|
|
||||||
"gohtmltmpl",
|
|
||||||
"haml",
|
|
||||||
"handlebars",
|
|
||||||
"hbs",
|
|
||||||
"html",
|
|
||||||
"htmlangular",
|
|
||||||
"html-eex",
|
|
||||||
"heex",
|
|
||||||
"jade",
|
|
||||||
"leaf",
|
|
||||||
"liquid",
|
|
||||||
"markdown",
|
|
||||||
"mdx",
|
|
||||||
"mustache",
|
|
||||||
"njk",
|
|
||||||
"nunjucks",
|
|
||||||
"php",
|
|
||||||
"razor",
|
|
||||||
"slim",
|
|
||||||
"twig",
|
|
||||||
-- css
|
|
||||||
"css",
|
|
||||||
"less",
|
|
||||||
"postcss",
|
|
||||||
"sass",
|
|
||||||
"scss",
|
|
||||||
"stylus",
|
|
||||||
"sugarss",
|
|
||||||
-- js
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"reason",
|
|
||||||
"rescript",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact",
|
|
||||||
-- mixed
|
|
||||||
"vue",
|
|
||||||
"svelte",
|
|
||||||
"templ",
|
|
||||||
},
|
},
|
||||||
settings = {
|
settings = {
|
||||||
tailwindCSS = {
|
tailwindCSS = {
|
||||||
validate = true,
|
validate = true,
|
||||||
lint = {
|
classAttributes = { "class", "className", "classList", "ngClass" },
|
||||||
cssConflict = "warning",
|
-- for cn()/clsx()/cva() wrappers:
|
||||||
invalidApply = "error",
|
experimental = {
|
||||||
invalidScreen = "error",
|
classRegex = {
|
||||||
invalidVariant = "error",
|
{ "cva%(([^)]*)%)", "[\"'`]([^\"'`]*).*?[\"'`]" },
|
||||||
invalidConfigPath = "error",
|
{ "cn%(([^)]*)%)", "[\"'`]([^\"'`]*).*?[\"'`]" },
|
||||||
invalidTailwindDirective = "error",
|
|
||||||
recommendedVariantOrder = "warning",
|
|
||||||
},
|
},
|
||||||
classAttributes = {
|
|
||||||
"class",
|
|
||||||
"className",
|
|
||||||
"class:list",
|
|
||||||
"classList",
|
|
||||||
"ngClass",
|
|
||||||
},
|
|
||||||
includeLanguages = {
|
|
||||||
eelixir = "html-eex",
|
|
||||||
elixir = "phoenix-heex",
|
|
||||||
eruby = "erb",
|
|
||||||
heex = "phoenix-heex",
|
|
||||||
htmlangular = "html",
|
|
||||||
templ = "html",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
before_init = function(_, config)
|
|
||||||
if not config.settings then
|
|
||||||
config.settings = {}
|
|
||||||
end
|
|
||||||
if not config.settings.editor then
|
|
||||||
config.settings.editor = {}
|
|
||||||
end
|
|
||||||
if not config.settings.editor.tabSize then
|
|
||||||
config.settings.editor.tabSize = vim.lsp.util.get_effective_tabstop()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
workspace_required = true,
|
|
||||||
root_dir = function(bufnr, on_dir)
|
|
||||||
local root_files = {
|
|
||||||
-- Generic
|
|
||||||
"tailwind.config.js",
|
|
||||||
"tailwind.config.cjs",
|
|
||||||
"tailwind.config.mjs",
|
|
||||||
"tailwind.config.ts",
|
|
||||||
"postcss.config.js",
|
|
||||||
"postcss.config.cjs",
|
|
||||||
"postcss.config.mjs",
|
|
||||||
"postcss.config.ts",
|
|
||||||
-- Django
|
|
||||||
"theme/static_src/tailwind.config.js",
|
|
||||||
"theme/static_src/tailwind.config.cjs",
|
|
||||||
"theme/static_src/tailwind.config.mjs",
|
|
||||||
"theme/static_src/tailwind.config.ts",
|
|
||||||
"theme/static_src/postcss.config.js",
|
|
||||||
-- Fallback for tailwind v4, where tailwind.config.* is not required anymore
|
|
||||||
".git",
|
|
||||||
}
|
|
||||||
local fname = vim.api.nvim_buf_get_name(bufnr)
|
|
||||||
root_files = insert_package_json(root_files, "tailwindcss", fname)
|
|
||||||
root_files = root_markers_with_field(root_files, { "mix.lock", "Gemfile.lock" }, "tailwind", fname)
|
|
||||||
on_dir(vim.fs.dirname(vim.fs.find(root_files, { path = fname, upward = true })[1]))
|
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
vim.lsp.enable("ts_ls")
|
vim.lsp.enable("ts_ls")
|
||||||
|
vim.lsp.enable("tailwindcss")
|
||||||
vim.lsp.enable("gopls")
|
vim.lsp.enable("gopls")
|
||||||
-- vim.lsp.enable("tailwindcss")
|
-- vim.lsp.enable("tailwindcss")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
{
|
||||||
|
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 = 300
|
||||||
|
on-timeout = loginctl lock-session
|
||||||
|
}
|
||||||
|
|
||||||
|
listener {
|
||||||
|
timeout = 330
|
||||||
|
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.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"
|
||||||
|
'';
|
||||||
|
|
||||||
|
waybarStyleSnippet = pkgs.writeText "waybar-power-profile.css" ''
|
||||||
|
/* @import this at the top of your `~/.config/waybar/style.css`:
|
||||||
|
@import url("power-profile.css");
|
||||||
|
*/
|
||||||
|
|
||||||
|
#custom-power-profile {
|
||||||
|
min-width: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-family: "BerkeleyMono Nerd Font Propo";
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-power-profile.caffeinated {
|
||||||
|
color: #fabd2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-power-profile.headless {
|
||||||
|
color: #83a598;
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-power-profile.normal {
|
||||||
|
color: #ebdbb2;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
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
|
||||||
|
];
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# Drop the waybar snippets where the user can @include them.
|
||||||
|
xdg.configFile."waybar/power-profile.jsonc".source = waybarConfigSnippet;
|
||||||
|
xdg.configFile."waybar/power-profile.css".source = waybarStyleSnippet;
|
||||||
|
|
||||||
|
# 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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-14
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Currently, the system uses a default power management configuration. There is no easy way for the user to switch between different power-saving modes for different use cases, such as presenting, running a headless server, or normal desktop usage.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Implement a script to control power management settings.
|
||||||
|
- Define three distinct power profiles: `caffeinated`, `headless`, and `normal`.
|
||||||
|
- Integrate the script with Hyprland for key-based switching.
|
||||||
|
- Provide visual feedback of the current profile in the Waybar status bar.
|
||||||
|
- Manage the entire configuration using Nix and Home Manager.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Creating a graphical user interface for configuration.
|
||||||
|
- Supporting operating systems other than NixOS.
|
||||||
|
- Supporting display managers other than Wayland/Hyprland.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **State Management**: A state file at `~/.local/state/power-profile` will store the name of the currently active profile (e.g., `caffeinated`). This simple approach allows the control script and Waybar module to easily share the current state.
|
||||||
|
|
||||||
|
2. **Control Script**: A `bash` script will be the core of this feature. It will accept arguments like `next`, `prev`, or a specific profile name (`caffeinated`, `headless`, `normal`). When called, it will:
|
||||||
|
a. Update the state file.
|
||||||
|
b. Kill any existing `hypridle` process.
|
||||||
|
c. Start a new `hypridle` process with the configuration corresponding to the selected profile.
|
||||||
|
d. Send a signal to Waybar to refresh the module (`pkill -RTMIN+8 waybar`).
|
||||||
|
|
||||||
|
3. **Power Management Daemon**: `hypridle` will be used to manage idle events. We will define three separate service configurations for it, one for each profile, with different timeouts for screen blanking (`hyprlock`), and system suspension.
|
||||||
|
- **Caffeinated**: No timeouts. `hypridle` will run with no events configured.
|
||||||
|
- **Headless**: Timeout for screen blanking and locking, but no suspend action.
|
||||||
|
- **Normal**: Default timeouts for screen blanking, locking, and suspension.
|
||||||
|
|
||||||
|
4. **Waybar Module**: A `custom/power-profile` module in Waybar will be used.
|
||||||
|
- It will use Waybar's `on-click` functionality to call the control script to cycle profiles.
|
||||||
|
- It will execute a small script (`exec`) that reads the state file and outputs JSON with an icon representing the current state (e.g., `{"text": "", "tooltip": "Profile: Caffeinated"}`).
|
||||||
|
- The module will update automatically when it receives a real-time signal, which the control script will send.
|
||||||
|
|
||||||
|
5. **Hyprland Keybinding**: A `bind` entry in `hyprland.conf` will be added to call the control script to cycle through the profiles, e.g., `bind = $mainMod, BRACKETRIGHT, exec, ~/dotfiles/scripts/power-profile.sh next`.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **[Risk]** The `pkill hypridle` command could potentially interfere with other user-started `hypridle` instances if they exist.
|
||||||
|
- **Mitigation**: This is unlikely in a typical user session. The script will be part of a managed dotfiles configuration where only one instance is expected.
|
||||||
|
- **Mitigation**: `systemctl --user restart hypridle.service` should restart hypridle with new configuration
|
||||||
|
|
||||||
|
- **[Trade-off]** Using a state file on disk introduces a small amount of I/O and a potential point of failure if file permissions are incorrect.
|
||||||
|
- **Rationale**: This is a simple and reliable IPC mechanism for this use case, preferable to more complex solutions. The file will be managed by Home Manager, ensuring correct permissions.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Users need a quick way to switch between different power management configurations on their system depending on the task at hand. For example, a "caffeinated" mode is needed to prevent the system from sleeping during presentations or long-running tasks, while a "headless" mode is useful for running the machine as a server, and a "normal" mode for everyday desktop use.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- A script will be created to manage three power management profiles: `caffeinated`, `headless`, and `normal`.
|
||||||
|
- A Waybar module will be added to display the current power profile.
|
||||||
|
- A Hyprland keybinding will be configured to cycle through the power profiles.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `power-profile-management`: Manages power-saving settings for different operational modes, including screen blanking, locking, and system suspension. It also provides a visual indicator for the current mode in the Waybar status bar.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Code:** New Nix code for the power management script and Waybar/Hyprland configuration.
|
||||||
|
- **System:** Affects system-wide power settings managed by `swayidle` or a similar service.
|
||||||
|
- **User Interface:** A new module will appear in the Waybar status bar.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Manage Power Profiles
|
||||||
|
The system SHALL provide three distinct power management profiles: `caffeinated`, `headless`, and `normal`.
|
||||||
|
|
||||||
|
#### Scenario: Caffeinated Profile
|
||||||
|
- **WHEN** the `caffeinated` profile is active
|
||||||
|
- **THEN** the system SHALL NOT blank the screen.
|
||||||
|
- **AND** the system SHALL NOT lock the screen.
|
||||||
|
- **AND** the system SHALL NOT suspend.
|
||||||
|
|
||||||
|
#### Scenario: Headless Profile
|
||||||
|
- **WHEN** the `headless` profile is active
|
||||||
|
- **THEN** the system SHALL blank the screen after a timeout.
|
||||||
|
- **AND** the system SHALL lock the screen when it blanks.
|
||||||
|
- **AND** the system SHALL NOT suspend.
|
||||||
|
|
||||||
|
#### Scenario: Normal Profile
|
||||||
|
- **WHEN** the `normal` profile is active
|
||||||
|
- **THEN** the system SHALL blank the screen after a default timeout.
|
||||||
|
- **AND** the system SHALL lock the screen when it blanks.
|
||||||
|
- **AND** the system SHALL suspend after a longer default timeout.
|
||||||
|
|
||||||
|
### Requirement: Cycle Through Power Profiles
|
||||||
|
The system SHALL allow the user to cycle through the power profiles using a keybinding.
|
||||||
|
|
||||||
|
#### Scenario: Cycle Forward
|
||||||
|
- **WHEN** the user presses the "next profile" keybinding
|
||||||
|
- **THEN** the active power profile SHALL change from `normal` to `caffeinated`, `caffeinated` to `headless`, and `headless` to `normal`.
|
||||||
|
|
||||||
|
#### Scenario: Cycle Backward
|
||||||
|
- **WHEN** the user presses the "previous profile" keybinding
|
||||||
|
- **THEN** the active power profile SHALL change from `normal` to `headless`, `headless` to `caffeinated`, and `caffeinated` to `normal`.
|
||||||
|
|
||||||
|
### Requirement: Visual Indicator for Current Profile
|
||||||
|
The system SHALL display the current power profile mode in the Waybar status bar.
|
||||||
|
|
||||||
|
#### Scenario: Display Caffeinated Icon
|
||||||
|
- **WHEN** the `caffeinated` profile is active
|
||||||
|
- **THEN** a "caffeinated" icon SHALL be visible in the Waybar status bar.
|
||||||
|
|
||||||
|
#### Scenario: Display Headless Icon
|
||||||
|
- **WHEN** the `headless` profile is active
|
||||||
|
- **THEN** a "headless" icon SHALL be visible in the Waybar status bar.
|
||||||
|
|
||||||
|
#### Scenario: Display Normal Icon
|
||||||
|
- **WHEN** the `normal` profile is active
|
||||||
|
- **THEN** a "normal" icon SHALL be visible in the Waybar status bar.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
## 1. State Management and Control Script
|
||||||
|
|
||||||
|
- [x] 1.1 Create the state directory `~/.local/state` if it doesn't exist.
|
||||||
|
- [x] 1.2 Create the control script `scripts/power-profile.sh`.
|
||||||
|
- [x] 1.3 Implement logic in the script to read and write the current profile to `~/.local/state/power-profile`.
|
||||||
|
- [x] 1.4 Implement `next` and `prev` logic to cycle through `normal`, `caffeinated`, and `headless`.
|
||||||
|
- [x] 1.5 Add logic to kill existing `swayidle` processes.
|
||||||
|
- [x] 1.6 Add logic to start `swayidle` with profile-specific arguments.
|
||||||
|
- [x] 1.7 Add logic to send a signal to Waybar to refresh the module (`pkill -RTMIN+8 waybar`).
|
||||||
|
|
||||||
|
## 2. NixOS and Home Manager Configuration
|
||||||
|
|
||||||
|
- [x] 2.1 Create a new Nix module `modules/power-profiles.nix` for the feature.
|
||||||
|
- [x] 2.2 In the module, define the three `swayidle` service configurations.
|
||||||
|
- [x] 2.3 Package the `power-profile.sh` script using `pkgs.writeShellScriptBin`.
|
||||||
|
- [x] 2.4 Add the script package to `home.packages`.
|
||||||
|
- [x] 2.5 Ensure the state directory is created using `home.file`.
|
||||||
|
|
||||||
|
## 3. Waybar Integration
|
||||||
|
|
||||||
|
- [x] 3.1 Add a `custom/power-profile` module to the Waybar configuration in `config/waybar/config`.
|
||||||
|
- [x] 3.2 Configure the module's `exec` to a script that reads the state file and outputs JSON with the correct icon and tooltip.
|
||||||
|
- [x] 3.3 Configure `on-click` to call `power-profile.sh next`.
|
||||||
|
- [x] 3.4 Configure `on-click-right` to call `power-profile.sh prev`.
|
||||||
|
- [x] 3.5 Add styles for the module in `config/waybar/style.css`.
|
||||||
|
- [x] 3.6 Define the real-time signal number for updates (e.g., `signal: 8`).
|
||||||
|
|
||||||
|
## 4. Hyprland Integration
|
||||||
|
|
||||||
|
- [x] 4.1 Add a keybinding to `config/hypr/hyprland.conf` to call `power-profile.sh next`.
|
||||||
|
- [x] 4.2 Add a keybinding to call `power-profile.sh prev`.
|
||||||
|
|
||||||
|
## 5. Verification
|
||||||
|
|
||||||
|
- [ ] 5.1 Rebuild the system with `home-manager switch`.
|
||||||
|
- [ ] 5.2 Verify that the Waybar module appears and shows the default `normal` state.
|
||||||
|
- [ ] 5.3 Test the keybindings to cycle through the profiles and observe the icon change.
|
||||||
|
- [ ] 5.4 Check that `swayidle` processes are restarted correctly with each profile change.
|
||||||
|
- [ ] 5.5 Verify the behavior of each profile (e.g., no suspension in `caffeinated` mode).
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
schema: spec-driven
|
schema: spec-driven
|
||||||
|
context: |
|
||||||
context: |
|
|
||||||
Tech stack: Unix System, NixOS, Flakes, Home-manager
|
Tech stack: Unix System, NixOS, Flakes, Home-manager
|
||||||
|
|||||||
Reference in New Issue
Block a user