From 2360d61ad40c3423bda71339ea806f3afc9dac75 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 7 May 2026 13:06:22 +0800 Subject: [PATCH 1/5] fix(noctalia): lock before lid suspend Add a user sleep-target hook that calls the same Noctalia lock IPC path as the manual Hyprland lock binding before system sleep continues. Co-authored-by: Codex --- config/noctalia/default.nix | 21 ++++++++++++++++++++- config/noctalia/lock-before-sleep.sh | 12 ++++++++++++ spec/coverage_spec.sh | 1 + spec/noctalia_lock_before_sleep_spec.sh | 24 ++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 config/noctalia/lock-before-sleep.sh create mode 100644 spec/noctalia_lock_before_sleep_spec.sh diff --git a/config/noctalia/default.nix b/config/noctalia/default.nix index 9ad825351..662e2a070 100644 --- a/config/noctalia/default.nix +++ b/config/noctalia/default.nix @@ -3,6 +3,13 @@ pkgs, ... }: +let + noctaliaShell = inputs.noctalia-shell.packages.${pkgs.system}.default; + noctaliaLockBeforeSleep = pkgs.replaceVars ./lock-before-sleep.sh { + noctalia_shell = "${noctaliaShell}/bin/noctalia-shell"; + sleep = "${pkgs.coreutils}/bin/sleep"; + }; +in { xdg.configFile."noctalia/colorschemes/Dracula-Custom/Dracula-Custom.json" = { source = ./Dracula-Custom.json; @@ -24,9 +31,21 @@ Install.WantedBy = [ "graphical-session.target" ]; }; + systemd.user.services.noctalia-lock-before-sleep = { + Unit = { + Description = "Lock Noctalia before system sleep"; + Before = [ "sleep.target" ]; + }; + Service = { + Type = "oneshot"; + ExecStart = "${pkgs.bash}/bin/bash ${noctaliaLockBeforeSleep}"; + }; + Install.WantedBy = [ "sleep.target" ]; + }; + programs.noctalia-shell = { enable = true; - package = inputs.noctalia-shell.packages.${pkgs.system}.default; + package = noctaliaShell; settings = { bar = { diff --git a/config/noctalia/lock-before-sleep.sh b/config/noctalia/lock-before-sleep.sh new file mode 100644 index 000000000..08cea745c --- /dev/null +++ b/config/noctalia/lock-before-sleep.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Lock through Noctalia before systemd lets suspend continue. +set -euo pipefail + +NOCTALIA_SHELL="@noctalia_shell@" +SLEEP="@sleep@" + +if ! "$NOCTALIA_SHELL" ipc call lockScreen lock; then + exit 0 +fi + +"$SLEEP" 1 diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index d90ccdb83..137dde576 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -370,6 +370,7 @@ config/hyprland/scripts/record-screen.sh config/hyprland/scripts/toggle-terminal.sh config/k3s/activate.sh config/noctalia/ac-idle-inhibit.sh +config/noctalia/lock-before-sleep.sh config/obsidian/activate.sh config/omp/activate.sh config/openclaw/hydrate.sh diff --git a/spec/noctalia_lock_before_sleep_spec.sh b/spec/noctalia_lock_before_sleep_spec.sh new file mode 100644 index 000000000..401f76a9a --- /dev/null +++ b/spec/noctalia_lock_before_sleep_spec.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016,SC2329 + +Describe 'config/noctalia/lock-before-sleep.sh' +SCRIPT="$PWD/config/noctalia/lock-before-sleep.sh" + +It 'uses bash strict mode' +When run bash -c "head -5 '$SCRIPT'" +The output should include 'set -euo pipefail' +End + +It 'uses injected command paths' +When run bash -c "cat '$SCRIPT'" +The output should include '@noctalia_shell@' +The output should include '@sleep@' +End + +It 'calls Noctalia lock IPC before sleeping' +When run bash -c "cat '$SCRIPT'" +The output should include 'ipc call lockScreen lock' +The output should include '"$SLEEP" 1' +End + +End From 5f3006a3dfa1b5c1e5d8fe34ad38fb9c20edfe29 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 7 May 2026 15:19:56 +0800 Subject: [PATCH 2/5] fix(noctalia): lock on lid close Add a graphical-session user service that watches the ACPI lid state and calls Noctalia's lock IPC as soon as the lid transitions to closed. Keep the pre-sleep lock as a fallback for actual suspend paths. Co-authored-by: Codex --- config/noctalia/default.nix | 19 ++++++++++++ config/noctalia/lid-lock.sh | 53 ++++++++++++++++++++++++++++++++++ spec/coverage_spec.sh | 1 + spec/noctalia_lid_lock_spec.sh | 30 +++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 config/noctalia/lid-lock.sh create mode 100644 spec/noctalia_lid_lock_spec.sh diff --git a/config/noctalia/default.nix b/config/noctalia/default.nix index 662e2a070..ff661c6bb 100644 --- a/config/noctalia/default.nix +++ b/config/noctalia/default.nix @@ -9,6 +9,10 @@ let noctalia_shell = "${noctaliaShell}/bin/noctalia-shell"; sleep = "${pkgs.coreutils}/bin/sleep"; }; + noctaliaLidLock = pkgs.replaceVars ./lid-lock.sh { + noctalia_shell = "${noctaliaShell}/bin/noctalia-shell"; + sleep = "${pkgs.coreutils}/bin/sleep"; + }; in { xdg.configFile."noctalia/colorschemes/Dracula-Custom/Dracula-Custom.json" = { @@ -43,6 +47,21 @@ in Install.WantedBy = [ "sleep.target" ]; }; + systemd.user.services.noctalia-lid-lock = { + Unit = { + Description = "Lock Noctalia when the lid closes"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + Type = "simple"; + ExecStart = "${pkgs.bash}/bin/bash ${noctaliaLidLock}"; + Restart = "on-failure"; + RestartSec = 2; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; + programs.noctalia-shell = { enable = true; package = noctaliaShell; diff --git a/config/noctalia/lid-lock.sh b/config/noctalia/lid-lock.sh new file mode 100644 index 000000000..1e998c07f --- /dev/null +++ b/config/noctalia/lid-lock.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Lock through Noctalia on the physical lid-close transition. +set -euo pipefail + +NOCTALIA_SHELL="@noctalia_shell@" +SLEEP="@sleep@" + +find_lid_state_file() { + local path + + for path in /proc/acpi/button/lid/*/state; do + [ -f "$path" ] || continue + printf '%s\n' "$path" + return 0 + done + + return 1 +} + +read_lid_state() { + local path="$1" + local line + + IFS= read -r line <"$path" || return 1 + + case "$line" in + *closed*) printf '%s\n' closed ;; + *open*) printf '%s\n' open ;; + *) printf '%s\n' unknown ;; + esac +} + +lock_noctalia() { + "$NOCTALIA_SHELL" ipc call lockScreen lock >/dev/null 2>&1 || true +} + +last_state="" + +while true; do + lid_state_file="$(find_lid_state_file || true)" + if [ -z "$lid_state_file" ]; then + "$SLEEP" 5 + continue + fi + + lid_state="$(read_lid_state "$lid_state_file" || printf '%s\n' unknown)" + if [ "$lid_state" = closed ] && [ "$last_state" != closed ]; then + lock_noctalia + fi + + last_state="$lid_state" + "$SLEEP" 1 +done diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 137dde576..3fe074ecb 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -370,6 +370,7 @@ config/hyprland/scripts/record-screen.sh config/hyprland/scripts/toggle-terminal.sh config/k3s/activate.sh config/noctalia/ac-idle-inhibit.sh +config/noctalia/lid-lock.sh config/noctalia/lock-before-sleep.sh config/obsidian/activate.sh config/omp/activate.sh diff --git a/spec/noctalia_lid_lock_spec.sh b/spec/noctalia_lid_lock_spec.sh new file mode 100644 index 000000000..1e1de36d8 --- /dev/null +++ b/spec/noctalia_lid_lock_spec.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016,SC2329 + +Describe 'config/noctalia/lid-lock.sh' +SCRIPT="$PWD/config/noctalia/lid-lock.sh" + +It 'uses bash strict mode' +When run bash -c "head -5 '$SCRIPT'" +The output should include 'set -euo pipefail' +End + +It 'uses injected command paths' +When run bash -c "cat '$SCRIPT'" +The output should include '@noctalia_shell@' +The output should include '@sleep@' +End + +It 'reads the ACPI lid state file' +When run bash -c "cat '$SCRIPT'" +The output should include '/proc/acpi/button/lid/*/state' +End + +It 'locks Noctalia on the closed transition' +When run bash -c "cat '$SCRIPT'" +The output should include 'ipc call lockScreen lock' +The output should include '[ "$lid_state" = closed ]' +The output should include '[ "$last_state" != closed ]' +End + +End From e8b8164ce622fbf2eb20e12adb7058138dc0d624 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 7 May 2026 15:23:05 +0800 Subject: [PATCH 3/5] ci(e2e): wait for pull request head ref Avoid racing GitHub's pull request ref propagation by checking that refs/pull//head is fetchable before invoking install.sh in E2E. Co-authored-by: Codex --- .github/workflows/e2e.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 33f7ac8ab..decc33c44 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -63,6 +63,20 @@ jobs: sudo mv "$file" "${file}.before-nix-darwin" fi done + - name: Wait for PR Head Ref + if: github.event_name == 'pull_request' + run: | + for attempt in 1 2 3 4 5 6; do + if git ls-remote --exit-code origin "refs/pull/${{ github.event.pull_request.number }}/head"; then + exit 0 + fi + + echo "PR head ref is not available yet; retrying in 10s (attempt ${attempt}/6)." + sleep 10 + done + + echo "PR head ref refs/pull/${{ github.event.pull_request.number }}/head is unavailable." + exit 1 - name: Run Install Command run: | curl -fsSL https://raw.githubusercontent.com/shunkakinoki/dotfiles/${{ github.sha }}/install.sh | sh From e6592d3ae5f8dd3bc55889af9d2e9abd4030f715 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 7 May 2026 15:48:26 +0800 Subject: [PATCH 4/5] fix(hyprland): lock noctalia on lid switch Hyprland sees the physical Lid Switch directly, while the previous ACPI polling service could miss the close event before logind handled suspend. Bind the switch-close event to the same Noctalia lock IPC as the working manual lock shortcut and remove the polling user service. Co-authored-by: Codex --- config/hyprland/hyprland.conf | 1 + config/noctalia/default.nix | 19 ------------ config/noctalia/lid-lock.sh | 53 ---------------------------------- spec/coverage_spec.sh | 1 - spec/hyprland_lid_lock_spec.sh | 13 +++++++++ spec/noctalia_lid_lock_spec.sh | 30 ------------------- 6 files changed, 14 insertions(+), 103 deletions(-) delete mode 100644 config/noctalia/lid-lock.sh create mode 100644 spec/hyprland_lid_lock_spec.sh delete mode 100644 spec/noctalia_lid_lock_spec.sh diff --git a/config/hyprland/hyprland.conf b/config/hyprland/hyprland.conf index 80170f2c4..83290770e 100644 --- a/config/hyprland/hyprland.conf +++ b/config/hyprland/hyprland.conf @@ -420,6 +420,7 @@ binde = CTRL ALT SHIFT SUPER, minus, exec, hyprctl -j monitors | jq -r '.[0].sca # Lock Screen # ============================================================================= bind = CTRL ALT SHIFT SUPER, L, exec, noctalia-shell ipc call lockScreen lock +bindl = , switch:on:Lid Switch, exec, noctalia-shell ipc call lockScreen lock # ============================================================================= # Notification Toggle diff --git a/config/noctalia/default.nix b/config/noctalia/default.nix index ff661c6bb..662e2a070 100644 --- a/config/noctalia/default.nix +++ b/config/noctalia/default.nix @@ -9,10 +9,6 @@ let noctalia_shell = "${noctaliaShell}/bin/noctalia-shell"; sleep = "${pkgs.coreutils}/bin/sleep"; }; - noctaliaLidLock = pkgs.replaceVars ./lid-lock.sh { - noctalia_shell = "${noctaliaShell}/bin/noctalia-shell"; - sleep = "${pkgs.coreutils}/bin/sleep"; - }; in { xdg.configFile."noctalia/colorschemes/Dracula-Custom/Dracula-Custom.json" = { @@ -47,21 +43,6 @@ in Install.WantedBy = [ "sleep.target" ]; }; - systemd.user.services.noctalia-lid-lock = { - Unit = { - Description = "Lock Noctalia when the lid closes"; - After = [ "graphical-session.target" ]; - PartOf = [ "graphical-session.target" ]; - }; - Service = { - Type = "simple"; - ExecStart = "${pkgs.bash}/bin/bash ${noctaliaLidLock}"; - Restart = "on-failure"; - RestartSec = 2; - }; - Install.WantedBy = [ "graphical-session.target" ]; - }; - programs.noctalia-shell = { enable = true; package = noctaliaShell; diff --git a/config/noctalia/lid-lock.sh b/config/noctalia/lid-lock.sh deleted file mode 100644 index 1e998c07f..000000000 --- a/config/noctalia/lid-lock.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# Lock through Noctalia on the physical lid-close transition. -set -euo pipefail - -NOCTALIA_SHELL="@noctalia_shell@" -SLEEP="@sleep@" - -find_lid_state_file() { - local path - - for path in /proc/acpi/button/lid/*/state; do - [ -f "$path" ] || continue - printf '%s\n' "$path" - return 0 - done - - return 1 -} - -read_lid_state() { - local path="$1" - local line - - IFS= read -r line <"$path" || return 1 - - case "$line" in - *closed*) printf '%s\n' closed ;; - *open*) printf '%s\n' open ;; - *) printf '%s\n' unknown ;; - esac -} - -lock_noctalia() { - "$NOCTALIA_SHELL" ipc call lockScreen lock >/dev/null 2>&1 || true -} - -last_state="" - -while true; do - lid_state_file="$(find_lid_state_file || true)" - if [ -z "$lid_state_file" ]; then - "$SLEEP" 5 - continue - fi - - lid_state="$(read_lid_state "$lid_state_file" || printf '%s\n' unknown)" - if [ "$lid_state" = closed ] && [ "$last_state" != closed ]; then - lock_noctalia - fi - - last_state="$lid_state" - "$SLEEP" 1 -done diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 3fe074ecb..137dde576 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -370,7 +370,6 @@ config/hyprland/scripts/record-screen.sh config/hyprland/scripts/toggle-terminal.sh config/k3s/activate.sh config/noctalia/ac-idle-inhibit.sh -config/noctalia/lid-lock.sh config/noctalia/lock-before-sleep.sh config/obsidian/activate.sh config/omp/activate.sh diff --git a/spec/hyprland_lid_lock_spec.sh b/spec/hyprland_lid_lock_spec.sh new file mode 100644 index 000000000..713bb159c --- /dev/null +++ b/spec/hyprland_lid_lock_spec.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'config/hyprland/hyprland.conf lid lock binding' +CONFIG="$PWD/config/hyprland/hyprland.conf" + +It 'locks Noctalia when Hyprland reports the lid switch closing' +When run bash -c "grep -F 'bindl = , switch:on:Lid Switch, exec, noctalia-shell ipc call lockScreen lock' '$CONFIG'" +The output should include 'switch:on:Lid Switch' +The output should include 'lockScreen lock' +End + +End diff --git a/spec/noctalia_lid_lock_spec.sh b/spec/noctalia_lid_lock_spec.sh deleted file mode 100644 index 1e1de36d8..000000000 --- a/spec/noctalia_lid_lock_spec.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2016,SC2329 - -Describe 'config/noctalia/lid-lock.sh' -SCRIPT="$PWD/config/noctalia/lid-lock.sh" - -It 'uses bash strict mode' -When run bash -c "head -5 '$SCRIPT'" -The output should include 'set -euo pipefail' -End - -It 'uses injected command paths' -When run bash -c "cat '$SCRIPT'" -The output should include '@noctalia_shell@' -The output should include '@sleep@' -End - -It 'reads the ACPI lid state file' -When run bash -c "cat '$SCRIPT'" -The output should include '/proc/acpi/button/lid/*/state' -End - -It 'locks Noctalia on the closed transition' -When run bash -c "cat '$SCRIPT'" -The output should include 'ipc call lockScreen lock' -The output should include '[ "$lid_state" = closed ]' -The output should include '[ "$last_state" != closed ]' -End - -End From aff66980929820678bc03ba164b59f1ce55fd279 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Thu, 7 May 2026 16:36:44 +0800 Subject: [PATCH 5/5] fix(noctalia): tighten lock behavior and controls Add the DarkMode widget immediately after Brightness on the right bar. Keep Noctalia lock-screen fingerprint auth loaded without pam_fprintd default try or timeout limits, and let Hyprland handle AC lid-close locking while battery lid close still suspends. Co-authored-by: Codex --- config/noctalia/default.nix | 1 + named-hosts/matic/default.nix | 8 ++++++-- spec/matic_lid_policy_spec.sh | 17 +++++++++++++++++ spec/matic_pam_fingerprint_spec.sh | 14 ++++++++++++++ spec/noctalia_bar_spec.sh | 12 ++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 spec/matic_lid_policy_spec.sh create mode 100644 spec/matic_pam_fingerprint_spec.sh create mode 100644 spec/noctalia_bar_spec.sh diff --git a/config/noctalia/default.nix b/config/noctalia/default.nix index 662e2a070..19f1b6a22 100644 --- a/config/noctalia/default.nix +++ b/config/noctalia/default.nix @@ -79,6 +79,7 @@ in { id = "PowerProfile"; } { id = "Volume"; } { id = "Brightness"; } + { id = "DarkMode"; } { id = "ControlCenter"; } ]; }; diff --git a/named-hosts/matic/default.nix b/named-hosts/matic/default.nix index 1d4967e69..5b230c57d 100644 --- a/named-hosts/matic/default.nix +++ b/named-hosts/matic/default.nix @@ -210,6 +210,10 @@ import ../../hosts/nixos { }; security.pam.services.noctalia-shell = { fprintAuth = true; + rules.auth.fprintd.settings = { + max-tries = -1; + timeout = -1; + }; }; security.pam.services.sudo = { fprintAuth = true; @@ -246,9 +250,9 @@ import ../../hosts/nixos { # Power button behavior - lock screen instead of shutdown services.logind.settings.Login.HandlePowerKey = "lock"; - # Suspend on lid close + # Suspend on battery lid close; on AC, Hyprland locks on the lid switch event. services.logind.settings.Login.HandleLidSwitch = "suspend"; - services.logind.settings.Login.HandleLidSwitchExternalPower = "suspend"; + services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore"; # Auto timezone (via geolocation) services.geoclue2.enable = true; diff --git a/spec/matic_lid_policy_spec.sh b/spec/matic_lid_policy_spec.sh new file mode 100644 index 000000000..b554416c7 --- /dev/null +++ b/spec/matic_lid_policy_spec.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'named-hosts/matic/default.nix lid policy' +CONFIG="$PWD/named-hosts/matic/default.nix" + +It 'keeps battery lid close as suspend' +When run bash -c "grep -F 'services.logind.settings.Login.HandleLidSwitch = \"suspend\";' '$CONFIG'" +The output should include 'HandleLidSwitch = "suspend"' +End + +It 'lets Hyprland handle lid close while on AC power' +When run bash -c "grep -F 'services.logind.settings.Login.HandleLidSwitchExternalPower = \"ignore\";' '$CONFIG'" +The output should include 'HandleLidSwitchExternalPower = "ignore"' +End + +End diff --git a/spec/matic_pam_fingerprint_spec.sh b/spec/matic_pam_fingerprint_spec.sh new file mode 100644 index 000000000..d08f478e4 --- /dev/null +++ b/spec/matic_pam_fingerprint_spec.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'named-hosts/matic/default.nix Noctalia fingerprint PAM' +CONFIG="$PWD/named-hosts/matic/default.nix" + +It 'keeps lock-screen fingerprint auth from timing out' +When run bash -c "awk '/security.pam.services.noctalia-shell = \\{/{in_service=1} in_service{print} in_service && /^ \\};/{exit}' '$CONFIG'" +The output should include 'fprintAuth = true;' +The output should include 'max-tries = -1;' +The output should include 'timeout = -1;' +End + +End diff --git a/spec/noctalia_bar_spec.sh b/spec/noctalia_bar_spec.sh new file mode 100644 index 000000000..4b7f21fbf --- /dev/null +++ b/spec/noctalia_bar_spec.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +Describe 'config/noctalia/default.nix bar widgets' +CONFIG="$PWD/config/noctalia/default.nix" + +It 'places DarkMode immediately after Brightness on the right bar' +When run bash -c "awk '/widgets.right = \\[/{in_right=1} in_right && /id =/ { gsub(/.*id = \"|\";.*/, \"\"); print } in_right && /\\];/{exit}' '$CONFIG' | paste -sd ' ' -" +The output should include 'Brightness DarkMode ControlCenter' +End + +End