diff --git a/hosts/jpporta-nixos/home.nix b/hosts/jpporta-nixos/home.nix index 265ab4e..50821ee 100644 --- a/hosts/jpporta-nixos/home.nix +++ b/hosts/jpporta-nixos/home.nix @@ -31,6 +31,7 @@ ../../modules/home-manager/pi ../../modules/home-manager/tmux ../../modules/home-manager/openspec + ../../modules/home-manager/power-profiles inputs.zen-browser.homeModules.beta ]; @@ -87,6 +88,7 @@ pi.enable = true; tmux.enable = true; openspec.enable = true; + power-profiles.enable = true; }; home.packages = diff --git a/modules/home-manager/hypridle/default.nix b/modules/home-manager/hypridle/default.nix index 0f41b81..90e4952 100644 --- a/modules/home-manager/hypridle/default.nix +++ b/modules/home-manager/hypridle/default.nix @@ -13,31 +13,6 @@ config = lib.mkIf config.custom.hypridle.enable { services.hypridle = { 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"; - } - ]; - }; }; }; } diff --git a/modules/home-manager/hyprland/default.nix b/modules/home-manager/hyprland/default.nix index 07c9965..2703e0e 100644 --- a/modules/home-manager/hyprland/default.nix +++ b/modules/home-manager/hyprland/default.nix @@ -152,7 +152,7 @@ in 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("waybar") - hl.exec_cmd("hypridle") + hl.exec_cmd("systemctl --user enable --now power-profile-hypridle.service") hl.exec_cmd("swaync") hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice-Right 24") end) @@ -258,6 +258,10 @@ in hl.bind(mod_alt .. " + L", hl.dsp.exec_cmd("hyprlock")) 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 ---------- hl.bind(mod_alt .. " + R", hl.dsp.exec_cmd("/home/jpporta/.config/waybar/scripts/launch.sh")) diff --git a/modules/home-manager/nvim/nvim/lsp/tailwindcss.lua b/modules/home-manager/nvim/nvim/lsp/tailwindcss.lua index 15ff9cd..1d8eeed 100644 --- a/modules/home-manager/nvim/nvim/lsp/tailwindcss.lua +++ b/modules/home-manager/nvim/nvim/lsp/tailwindcss.lua @@ -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 { - cmd = { "tailwindcss-language-server", "--stdio" }, - -- filetypes copied and adjusted from tailwindcss-intellisense - filetypes = { - -- html - "aspnetcorerazor", - "astro", - "astro-markdown", - "blade", - "clojure", - "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 = { - tailwindCSS = { - validate = true, - lint = { - cssConflict = "warning", - invalidApply = "error", - invalidScreen = "error", - invalidVariant = "error", - invalidConfigPath = "error", - 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, + cmd = { "tailwindcss-language-server", "--stdio" }, + filetypes = { + "html", "css", "scss", "javascript", "javascriptreact", + "typescript", "typescriptreact", "svelte", "vue", "astro", + }, + root_markers = { + "tailwind.config.js", "tailwind.config.cjs", "tailwind.config.ts", + "postcss.config.js", "package.json", ".git", + }, + settings = { + tailwindCSS = { + validate = true, + classAttributes = { "class", "className", "classList", "ngClass" }, + -- for cn()/clsx()/cva() wrappers: + experimental = { + classRegex = { + { "cva%(([^)]*)%)", "[\"'`]([^\"'`]*).*?[\"'`]" }, + { "cn%(([^)]*)%)", "[\"'`]([^\"'`]*).*?[\"'`]" }, + }, + }, + }, + }, } diff --git a/modules/home-manager/nvim/nvim/lua/jpporta/lsp.lua b/modules/home-manager/nvim/nvim/lua/jpporta/lsp.lua index 40ce002..986a0c8 100644 --- a/modules/home-manager/nvim/nvim/lua/jpporta/lsp.lua +++ b/modules/home-manager/nvim/nvim/lua/jpporta/lsp.lua @@ -1,4 +1,5 @@ vim.lsp.enable("ts_ls") +vim.lsp.enable("tailwindcss") vim.lsp.enable("gopls") -- vim.lsp.enable("tailwindcss") diff --git a/modules/home-manager/power-profiles/default.nix b/modules/home-manager/power-profiles/default.nix new file mode 100644 index 0000000..58baa8a --- /dev/null +++ b/modules/home-manager/power-profiles/default.nix @@ -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`, ``, 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 + ''; + }; +} diff --git a/openspec/changes/power-management-profiles/.openspec.yaml b/openspec/changes/power-management-profiles/.openspec.yaml new file mode 100644 index 0000000..64105fc --- /dev/null +++ b/openspec/changes/power-management-profiles/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-14 diff --git a/openspec/changes/power-management-profiles/design.md b/openspec/changes/power-management-profiles/design.md new file mode 100644 index 0000000..1647fd8 --- /dev/null +++ b/openspec/changes/power-management-profiles/design.md @@ -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. diff --git a/openspec/changes/power-management-profiles/proposal.md b/openspec/changes/power-management-profiles/proposal.md new file mode 100644 index 0000000..4483502 --- /dev/null +++ b/openspec/changes/power-management-profiles/proposal.md @@ -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. diff --git a/openspec/changes/power-management-profiles/specs/power-profile-management/spec.md b/openspec/changes/power-management-profiles/specs/power-profile-management/spec.md new file mode 100644 index 0000000..608183b --- /dev/null +++ b/openspec/changes/power-management-profiles/specs/power-profile-management/spec.md @@ -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. diff --git a/openspec/changes/power-management-profiles/tasks.md b/openspec/changes/power-management-profiles/tasks.md new file mode 100644 index 0000000..3449210 --- /dev/null +++ b/openspec/changes/power-management-profiles/tasks.md @@ -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). diff --git a/openspec/config.yaml b/openspec/config.yaml index 621f351..665dc1c 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,4 +1,3 @@ schema: spec-driven - - context: | - Tech stack: Unix System, NixOS, Flakes, Home-manager +context: | + Tech stack: Unix System, NixOS, Flakes, Home-manager