Skip to content

fix(noctalia): lock before lid suspend - #1688

Merged
shunkakinoki merged 7 commits into
mainfrom
fix/noctalia-lid-lock
May 7, 2026
Merged

fix(noctalia): lock before lid suspend#1688
shunkakinoki merged 7 commits into
mainfrom
fix/noctalia-lid-lock

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a Home Manager user sleep.target hook that locks Noctalia before suspend
  • add a graphical-session noctalia-lid-lock service that watches the ACPI lid state and locks as soon as the lid transitions to closed
  • use external substituted scripts wired with pkgs.replaceVars, matching the repo's no-inline-script convention
  • add an E2E workflow guard that waits for GitHub's PR head ref before invoking install.sh

Validation

  • make shell-inline-check
  • make nix-format-check
  • make shell-check
  • shellspec spec/noctalia_lid_lock_spec.sh spec/noctalia_lock_before_sleep_spec.sh spec/coverage_spec.sh
  • nix run nixpkgs#actionlint -- .github/workflows/e2e.yml
  • make build && make switch
  • verified noctalia-lid-lock.service and noctalia-lock-before-sleep.service are enabled
  • verified noctalia-lid-lock.service is active after switch

@indent-zero

indent-zero Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Replaces the originally proposed pre-sleep user-systemd hook with a Hyprland-driven lid lock plus a host-level lid-policy split. The Hyprland bindl binding catches the lid switch event-driven and reuses the manual lock IPC; on AC, HandleLidSwitchExternalPower = "ignore" lets Hyprland alone handle the lid (no suspend), while battery lid close still suspends through systemd-logind. The pre-sleep oneshot is kept as a non-lid fallback. Also tightens fprintd PAM for the lock screen and adds a DarkMode bar widget; introduces a CI retry around git ls-remote refs/pull/<n>/head to fix the earlier transient e2e failure.

  • config/hyprland/hyprland.conf: adds bindl = , switch:on:Lid Switch, exec, noctalia-shell ipc call lockScreen lock next to the manual lock keybind.
  • named-hosts/matic/default.nix: switches HandleLidSwitchExternalPower from suspend to ignore (AC); battery path remains suspend. Adds rules.auth.fprintd.settings = { max-tries = -1; timeout = -1; } to the noctalia-shell PAM service.
  • config/noctalia/default.nix: adds a DarkMode widget after Brightness on the right bar; keeps systemd.user.services.noctalia-lock-before-sleep (externalized lock-before-sleep.sh, set -euo pipefail, fail-open on IPC error) as a non-lid suspend fallback.
  • New shellspec coverage: spec/hyprland_lid_lock_spec.sh, spec/matic_lid_policy_spec.sh, spec/matic_pam_fingerprint_spec.sh, spec/noctalia_bar_spec.sh, plus the existing spec/noctalia_lock_before_sleep_spec.sh and coverage_spec.sh registration.
  • .github/workflows/e2e.yml: 6 \u00d7 10s retry waiting for refs/pull/<number>/head before invoking install.sh.

Issues

5 potential issues found:

  • Latent (trigger: slow GPU init / cold start / high load): the hard-coded sleep 1 after the lock IPC may be too short for the noctalia lock surface to paint, leaving the desktop briefly visible on resume. Consider waiting on a deterministic signal (e.g., the noctalia lock state) instead of a fixed sleep. → Autofix
  • This systemd hook overlaps with general.lockOnSuspend = true already set on noctalia (line 102) without an inline comment explaining why both exist; future cleanup risks regressing the lid-close lock when one is removed as redundant. Add a short comment pointing to the lid-suspend code path the systemd hook is backstopping. → Autofix
  • The kept-as-fallback noctalia-lock-before-sleep user unit still won't run on system suspend: user-instance sleep.target is not activated by system suspend (open RFE: systemd#40387). Lid-close is now covered by the Hyprland bindl binding, but other suspend paths (systemctl suspend, idle-triggered suspend, power button if remapped) won't lock through this fallback. Either remove the dead fallback unit, or add a system-level proxy (user-sleep@.service doing systemctl --user start sleep.target) so the fallback actually works. → Autofix
  • Latent (trigger: lid close while on battery) — HandleLidSwitch = "suspend" still kicks off the suspend chain immediately, while the Hyprland bindl only async-forks noctalia-shell ipc call lockScreen lock. There is no synchronization between the two, so the kernel can suspend before noctalia processes the IPC, leaving the desktop briefly visible on resume. AC path is fine because HandleLidSwitchExternalPower = "ignore" removes the suspend race. → Autofix
  • Inconsistent with sibling user services in the same file: ac-idle-inhibit uses After/PartOf = [ "graphical-session.target" ], but noctalia-lock-before-sleep has neither. If sleep.target ever activates while the graphical session is down, the IPC call no-ops silently. Add the same ordering for consistency and to avoid no-op runs. → Autofix
3 issues already resolved
  • /proc/acpi/button/lid/*/state is the deprecated ACPI sysfs interface and is absent on some kernels (notably builds without CONFIG_ACPI_BUTTON proc entries). The script silently degrades to sleep 5 forever with no log line, so a user moving to such a kernel would lose lid-lock without any signal in journalctl. Log a warning the first time the file is missing. (fixed by commit e6592d3)
  • IPC failure is silent: if noctalia-shell ipc call lockScreen lock returns non-zero, the script still exits 0 (because of the trailing sleep 1), so systemctl --user status shows success and a regression (e.g., upstream renames the verb or the shell isn't running) won't surface in journalctl. Add at least an || logger ... to leave a trace. (fixed by commit 2360d61)
  • lid-lock.sh polling races with HandleLidSwitch=suspend: when the lid closes, systemd-logind starts suspend in tens to hundreds of ms, but the daemon may be mid-sleep 1. If suspend wins the race, the lock IPC never reaches noctalia before kernel suspend — exactly the scenario the PR is trying to fix. Subscribe to logind's PrepareForSleep D-Bus signal (or use an event-driven source like acpi_listen/udevadm monitor) instead of polling. (fixed by commit e6592d3)

CI Checks

e2e-check is red because all three e2e-run matrix jobs (NixOS, MacOS, Ubuntu) failed in the install.sh step with fatal: couldn't find remote ref refs/pull/1688/head. This is unrelated to the noctalia config change in this PR — install.sh tries to clone the dotfiles repo and check out refs/pull/${GITHUB_PR}/head, but that ref wasn't visible to git at the moment the job ran. Likely a transient CI/repo-sync race; rerunning the failed jobs should clear it.

Failing e2e-check
  • All three e2e-run matrix jobs (NixOS, MacOS, Ubuntu) failed at the curl ... | sh step that runs install.sh, with fatal: couldn't find remote ref refs/pull/1688/head when cloning the dotfiles repo. install.sh keys off GITHUB_PR=1688 and fetches refs/pull/1688/head; the ref wasn't resolvable from github.com/shunkakinoki/dotfiles.git at that moment. This is a transient CI-side issue, not a problem with the PR's diff (the only change is config/noctalia/default.nix and isn't exercised by the install step). Re-running the failed e2e-run jobs should make CI green.

⚡ Autofix All Issues

@mesa-dot-dev

mesa-dot-dev Bot commented May 7, 2026

Copy link
Copy Markdown

You do not have enough credits to review this pull request. Please purchase more credits to continue.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Lock screen now automatically engages when the device lid closes.
    • Added DarkMode widget to the status bar.
  • Improvements

    • Enhanced fingerprint authentication handling for the lock screen.
    • Refined lid-switch behavior for battery and external power modes.
  • Tests

    • Added comprehensive test coverage for new features and configurations.
  • Chores

    • Improved continuous integration workflow reliability.

Walkthrough

Adds a lock-before-sleep script and a systemd user oneshot service that runs it before sleep.target; adds a Hyprland lid-triggered lock binding, updates noctalia package wiring and bar widgets, adjusts PAM/logind host settings, adds specs, and adds a CI polling step.

Changes

Screen Lock Before Sleep

Layer / File(s) Summary
Bindings & Script
config/noctalia/default.nix, config/noctalia/lock-before-sleep.sh
Adds local noctaliaShell binding and generates noctaliaLockBeforeSleep from lock-before-sleep.sh; new script implements IPC lock then sleeps.
Service Configuration
config/noctalia/default.nix
Adds systemd.user.services.noctalia-lock-before-sleep oneshot installed for and ordered before sleep.target.
Package Wiring & UI
config/noctalia/default.nix
programs.noctalia-shell.package now references local noctaliaShell; inserts DarkMode widget into bar.widgets.right.
Hyprland Binding
config/hyprland/hyprland.conf
Adds bindl for switch:on:Lid Switch executing noctalia-shell ipc call lockScreen lock.
Host PAM / logind
named-hosts/matic/default.nix
Sets security.pam.services.noctalia-shell.rules.auth.fprintd.settings to unlimited max-tries/timeout; sets Login.HandleLidSwitch = "suspend" and HandleLidSwitchExternalPower = "ignore".
Tests / Coverage
spec/*, spec/coverage_spec.sh
Adds specs for lid-lock behavior, PAM settings, bar widget order, lock-before-sleep script; updates coverage list to include new script.
CI Workflow
.github/workflows/e2e.yml
Adds a Wait for PR Head Ref step that polls git ls-remote on pull_request runs with retries and delay.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop, I nudge, I press the key,

the screen locks tight before sleep's decree,
A script, a service, a lid's small knock,
The rabbit guards the sleeping dock,
Bright DarkMode winks — the system's lock.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(noctalia): lock before lid suspend' directly and clearly summarizes the primary change: adding functionality to lock Noctalia before the system suspends due to lid closure or sleep.target events.
Description check ✅ Passed The description is well-related to the changeset, providing a structured summary of changes (sleep.target hook, lid-lock service, script substitution, E2E workflow guard) and validation steps performed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/noctalia-lid-lock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mesa-dot-dev

mesa-dot-dev Bot commented May 7, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

  • add a Home Manager user sleep.target hook that locks Noctalia before suspend
  • add a graphical-session noctalia-lid-lock service that watches the ACPI lid state and locks as soon as the lid transitions to closed
  • use external substituted scripts wired with pkgs.replaceVars, matching the repo's no-inline-script convention
  • add an E2E workflow guard that waits for GitHub's PR head ref before invoking install.sh

What changed?

  • config/noctalia/default.nix: Introduced a new systemd user service noctalia-lock-before-sleep to execute a generated script before the system enters sleep, injecting necessary script paths. Additionally, the programs.noctalia-shell.package definition was refactored to use a local variable.
  • config/noctalia/lock-before-sleep.sh: Updated the script responsible for locking the screen before the system enters a sleep state, potentially involving changes to the locking mechanism, its conditions, or the specific command used for the lock.
  • spec/coverage_spec.sh: Updated to include config/noctalia/lock-before-sleep.sh in its coverage testing.
  • spec/noctalia_lock_before_sleep_spec.sh: New test file introducing RSpec-style tests for the config/noctalia/lock-before-sleep.sh script, ensuring it adheres to bash strict mode, uses injected command paths, and correctly invokes ipc call lockScreen lock before sleeping.
  • .github/workflows/e2e.yml: Added a "Wait for PR Head Ref" step to introduce a retry mechanism, ensuring the pull request's head reference is available before subsequent steps, improving workflow robustness for PR events.
  • config/hyprland/hyprland.conf: Added a new Hyprland binding to automatically lock the screen when the laptop lid is closed, executing the command noctalia-shell ipc call lockScreen lock.
  • spec/hyprland_lid_lock_spec.sh: New test file added to verify that the Hyprland configuration correctly binds the laptop lid close event to execute the noctalia-shell lock screen command.

Description generated by Mesa. Update settings

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new systemd user service, noctalia-lock-before-sleep, designed to lock the screen via noctalia-shell before the system enters a sleep state. Feedback highlights that sleep.target is a system-level target and may not correctly trigger a user-level service without a proxy. Additionally, the service appears to duplicate existing configuration settings, and the use of a manual sleep command to prevent race conditions is noted as a fragile workaround that should be replaced with a synchronous IPC call.

systemd.user.services.noctalia-lock-before-sleep = {
Unit = {
Description = "Lock Noctalia before system sleep";
Before = [ "sleep.target" ];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

sleep.target is a system-level target. By default, systemd --user instances do not have a sleep.target, so this service will not be triggered upon suspension unless a proxy like systemd-lock-handler is used. For a more robust implementation, a system-level service or a dedicated locker manager is typically required.

Install.WantedBy = [ "graphical-session.target" ];
};

systemd.user.services.noctalia-lock-before-sleep = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This service duplicates the functionality of the lockOnSuspend = true; setting at line 102. If the built-in setting is non-functional, consider removing it or adding a comment explaining why this manual service is necessary to avoid maintainability issues.

Comment thread config/noctalia/default.nix Outdated
Type = "oneshot";
ExecStart = pkgs.writeShellScript "noctalia-lock-before-sleep" ''
${inputs.noctalia-shell.packages.${pkgs.system}.default}/bin/noctalia-shell ipc call lockScreen lock
${pkgs.coreutils}/bin/sleep 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sleep 1 command is a fragile workaround for the race condition between the IPC call and system suspension. It is better to use a synchronous IPC call if noctalia-shell supports one, ensuring the lock is fully active before the script exits and the system suspends.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/noctalia/default.nix`:
- Around line 34-37: The current ExecStart script (created via
pkgs.writeShellScript "noctalia-lock-before-sleep") masks failures because
`${inputs.noctalia-shell…}/bin/noctalia-shell ipc call lockScreen lock` is
followed by `${pkgs.coreutils}/bin/sleep 1`; ensure the IPC command's exit
status is propagated by changing the script to either enable strict error
handling (e.g., set -e) at the top of the script or capture the exit code of
`noctalia-shell ipc call lockScreen lock` and `exit` with that code before
running `sleep 1`, so the service will fail (non-zero) when the IPC call fails
rather than always succeeding due to the trailing `sleep`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11474d69-13f0-49c6-b674-66b34972533c

📥 Commits

Reviewing files that changed from the base of the PR and between f2dd16d and fd88f09.

📒 Files selected for processing (1)
  • config/noctalia/default.nix

Comment thread config/noctalia/default.nix Outdated
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 <noreply@openai.com>
@shunkakinoki
shunkakinoki force-pushed the fix/noctalia-lid-lock branch from fd88f09 to 2360d61 Compare May 7, 2026 05:12
Comment thread config/noctalia/default.nix Outdated
Type = "oneshot";
ExecStart = pkgs.writeShellScript "noctalia-lock-before-sleep" ''
${inputs.noctalia-shell.packages.${pkgs.system}.default}/bin/noctalia-shell ipc call lockScreen lock
${pkgs.coreutils}/bin/sleep 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock UI render race: this sleep 1 is a heuristic — if the noctalia lock surface needs more than ~1s to render (cold start, slow GPU init, system under load), the kernel suspends before the lock is on screen, so the desktop is briefly visible on resume. There's no deterministic synchronization here; the script returns 0 either way and the system happily proceeds to suspend. Consider either (a) polling for lock-ready via a noctalia IPC status call with a bounded timeout, or (b) bumping the sleep with a comment explaining the trade-off so it isn't trimmed back later.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="config/noctalia/default.nix">

<violation number="1" location="config/noctalia/default.nix:30">
P1: Don't hook a user service into sleep.target; user managers don't get that target, so the pre-suspend lock won't fire.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread config/noctalia/default.nix
Comment thread config/noctalia/default.nix Outdated
systemd.user.services.noctalia-lock-before-sleep = {
Unit = {
Description = "Lock Noctalia before system sleep";
Before = [ "sleep.target" ];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing graphical-session ordering: the sibling ac-idle-inhibit unit (lines 13-25) uses After = [ "graphical-session.target" ] and PartOf = [ "graphical-session.target" ], but this new unit has neither. In edge sequences where sleep.target activates while graphical-session.target is inactive, the noctalia-shell IPC call will fail silently (and the script still exits 0). Adding After = [ "graphical-session.target" ] keeps the file consistent and makes the dependency explicit; avoid PartOf here since you don't want the unit stopped mid-suspend.

Install.WantedBy = [ "graphical-session.target" ];
};

systemd.user.services.noctalia-lock-before-sleep = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document the overlap with general.lockOnSuspend: noctalia is already configured with general.lockOnSuspend = true on line 102, so it isn't obvious from the diff alone that this systemd unit is intentionally a second mechanism rather than a duplicate. The commit message explains it (lid-suspend bypasses the in-app hook), but that context is lost once merged. A one-line comment above the unit pointing to services.logind.settings.Login.HandleLidSwitch in named-hosts/matic/default.nix will prevent a future cleanup pass from deleting one of the two and silently regressing lid-close locking.

@mesa-dot-dev

mesa-dot-dev Bot commented May 7, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Lock Noctalia before suspend to prevent the system sleeping with an unlocked session. Mirrors the Hyprland manual lock command for consistent behavior on lid close.

What changed?

  • config/noctalia/default.nix: Introduces a new systemd user service, noctalia-lock-before-sleep, which ensures Noctalia is locked before the system enters sleep. This service executes noctalia-shell ipc call lockScreen lock and pauses for one second, running prior to sleep.target.

Description generated by Mesa. Update settings

${pkgs.coreutils}/bin/sleep 1
'';
};
Install.WantedBy = [ "sleep.target" ];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unit will likely never run on system suspend. systemd's user-instance sleep.target is not automatically activated when the system enters sleep — there is no built-in bridge from system to per-user systemd manager. This is a long-standing systemd limitation (open RFE: systemd#40387, prior RFE #15477 closed without implementation), and the NixOS Power Management wiki only documents system-level wantedBy = [ "post-resume.target" ] for this reason. I grepped the rest of the dotfiles and there's no user-sleep@.service proxy or systemd-lock-handler configured to bridge the gap. The fix is one of:

  1. Move this to a system-level systemd.services.* (in named-hosts/matic/default.nix) with wantedBy = [ "sleep.target" ]; before = [ "sleep.target" ]; and run as your user with XDG_RUNTIME_DIR set so the IPC reaches noctalia.
  2. Add a system-level proxy unit user-sleep@.service that does systemctl --user start sleep.target — then this existing user unit fires.
  3. Have a system-level Before=sleep.target unit just call loginctl lock-session, and let noctalia react via the login1 Lock D-Bus signal (this is essentially what general.lockOnSuspend = true already does, which would make the whole new unit unnecessary if the lid path can be made to lock-session correctly).

Please verify with journalctl --user -u noctalia-lock-before-sleep after a real suspend/resume cycle before merging — I expect the unit to never have a recorded activation.

(Apologies for missing this in my initial review — I assumed modern systemd propagates user sleep.target, but the sources above show it does not.)

shunkakinoki and others added 4 commits May 7, 2026 15:14
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 <noreply@openai.com>
Avoid racing GitHub's pull request ref propagation by checking that refs/pull/<number>/head is fetchable before invoking install.sh in E2E.

Co-authored-by: Codex <noreply@openai.com>
Comment thread config/noctalia/lid-lock.sh Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="config/noctalia/lid-lock.sh">

<violation number="1" location="config/noctalia/lid-lock.sh:47">
P2: The first observed `closed` state is treated as a lid-close transition, causing an unnecessary lock when the script starts with the lid already closed.</violation>
</file>

<file name="config/noctalia/default.nix">

<violation number="1" location="config/noctalia/default.nix:52">
P3: Gate this lid-lock service with a unit condition so it does not run a permanent polling loop on systems without a lid state file.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread config/noctalia/lid-lock.sh Outdated
Comment thread config/noctalia/default.nix Outdated
shunkakinoki and others added 2 commits May 7, 2026 15:48
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 <noreply@openai.com>
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 <noreply@openai.com>
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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Battery lid-close still has a lock-vs-suspend race. With HandleLidSwitch = "suspend", systemd-logind starts the suspend chain immediately on lid close. The Hyprland bindl binding (added in this PR) async-forks noctalia-shell ipc call lockScreen lock in parallel — there’s no synchronization, so the kernel can win the race and suspend before noctalia has processed the IPC, leaving the desktop briefly visible on resume. The fallback noctalia-lock-before-sleep user unit doesn’t help here either (user-instance sleep.target isn’t bridged from system suspend).

The AC path you just added (HandleLidSwitchExternalPower = "ignore") sidesteps this because no suspend occurs. To close the gap on battery, either:

  1. Set HandleLidSwitch = "ignore" for battery too, and let a Hyprland binding sequence lock-then-suspend (exec, noctalia-shell ipc call lockScreen lock && sleep 0.5 && systemctl suspend), or
  2. Add a system-level systemd.services.lock-before-sleep ordered Before=sleep.target that calls loginctl lock-session (logind blocks suspend on the inhibitor until the unit completes; noctalia’s own login1 Lock subscription via general.lockOnSuspend = true then locks synchronously).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
spec/coverage_spec.sh (1)

7-311: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing It entry for config/noctalia/lock-before-sleep.sh in the spec-existence block.

config/noctalia/lock-before-sleep.sh is correctly added to covered_scripts (line 373), but the Describe 'all required scripts have spec files' block doesn't get a corresponding path-existence check. If spec/noctalia_lock_before_sleep_spec.sh is deleted, nothing here will catch it.

➕ Proposed addition (insert after line 209, following the same pattern)
+It 'has spec file for config/noctalia/lock-before-sleep.sh'
+The path "spec/noctalia_lock_before_sleep_spec.sh" should be exist
+End
+
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/coverage_spec.sh` around lines 7 - 311, The Describe 'all required
scripts have spec files' block is missing an It test for
config/noctalia/lock-before-sleep.sh; add an It that asserts the path
"spec/noctalia_lock_before_sleep_spec.sh" should be exist (matching the pattern
used for other entries) and insert it near the other entries (after the block
handling similar home-manager/config entries, e.g., following the test around
config/paperclip/hydrate.sh) so the presence of
spec/noctalia_lock_before_sleep_spec.sh is validated alongside covered_scripts.
🧹 Nitpick comments (1)
spec/noctalia_bar_spec.sh (1)

8-9: ⚡ Quick win

awk pattern is fragile against minor Nix formatting variations.

The gsub(/.*id = \"|\";.*/, "") pattern assumes every widget ID line is formatted as exactly id = "Foo"; with a trailing semicolon and no surrounding content after the semicolon. It will silently produce an empty string (and the test will fail without clear indication why) if alejandra/nixfmt reformats the line — e.g., dropping the trailing semicolon inside { }, adding a comment, or using id="Foo" without spaces.

Additionally, the test only asserts include 'Brightness DarkMode ControlCenter' rather than exact adjacency; if extra widgets are inserted between Brightness and DarkMode the substring check would still fail correctly, but it would also fail if the three widgets are non-contiguous even when ordering is otherwise correct. This is the intended behaviour, so it's fine — just worth noting.

Consider a slightly more resilient extraction:

♻️ More resilient awk alternative
- When run bash -c "awk '/widgets.right = \\[/{in_right=1} in_right && /id =/ { gsub(/.*id = \"|\";.*/, \"\"); print } in_right && /\\];/{exit}' '$CONFIG' | paste -sd ' ' -"
+ When run bash -c "awk '/widgets\\.right *= *\\[/{in_right=1} in_right && /id *=/ { match(\$0,/\"[^\"]+\"/); print substr(\$0,RSTART+1,RLENGTH-2) } in_right && /\\];/{exit}' '$CONFIG' | paste -sd ' ' -"

Using match + substr to extract the quoted value is immune to content before/after the quotes and handles both id = "Foo" and id="Foo".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/noctalia_bar_spec.sh` around lines 8 - 9, The current awk gsub(/.*id =
\"|\";.*/, "") is fragile to spacing, optional semicolons, or trailing comments;
update the awk command that extracts widget IDs to use a robust quoted-value
extraction (e.g. use match() and substr() to find the first quoted string after
an id token or a regex that captures id\s*=\s*"([^"]*)") so it correctly handles
variants like id="Foo", id = "Foo"; and trailing comments; keep the surrounding
logic (the in_right flag and the right-section exit) intact so the output still
lists the widget ids in order for the test assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/e2e.yml:
- Around line 69-70: The guard loop is checking the wrong ref (refs/pull/${{
github.event.pull_request.number }}/head) while the download uses ${{ github.sha
}}, which on pull_request events refers to the merge commit; update the check to
wait for the exact SHA you later use—either test availability of ${{ github.sha
}} directly or switch the download to use ${{ github.event.pull_request.head.sha
}} so both the guard and fetch use the same ref; ensure the loop command that
currently references refs/pull/... is replaced to verify the chosen SHA/ref
(github.sha or github.event.pull_request.head.sha) before proceeding to
raw.githubusercontent.com.

In `@config/noctalia/lock-before-sleep.sh`:
- Around line 8-10: The script currently exits silently when NOCTALIA_SHELL ipc
call lockScreen lock fails; update the failure branch in lock-before-sleep.sh to
call the injected logger (the `@logger`@ variable provided via pkgs.replaceVars)
with a clear message including the command and its exit status before calling
exit 0 so the journal records the lock failure (refer to NOCTALIA_SHELL and the
ipc call "lockScreen lock" to find the branch to modify).

In `@spec/noctalia_bar_spec.sh`:
- Line 5: CONFIG is currently set using $PWD which can be wrong depending on how
ShellSpec is invoked; change the assignment to use ShellSpec's %SPECROOT (e.g.
CONFIG="%SPECROOT/config/noctalia/default.nix") or compute a path anchored to
the spec file (using dirname on $BASH_SOURCE or $SPEC) so it always resolves
relative to the spec, and add a guard immediately after (e.g. test -r "$CONFIG"
|| shellspec_fail "config not found: $CONFIG") so the spec fails clearly instead
of letting the downstream awk pipeline produce no output.

---

Outside diff comments:
In `@spec/coverage_spec.sh`:
- Around line 7-311: The Describe 'all required scripts have spec files' block
is missing an It test for config/noctalia/lock-before-sleep.sh; add an It that
asserts the path "spec/noctalia_lock_before_sleep_spec.sh" should be exist
(matching the pattern used for other entries) and insert it near the other
entries (after the block handling similar home-manager/config entries, e.g.,
following the test around config/paperclip/hydrate.sh) so the presence of
spec/noctalia_lock_before_sleep_spec.sh is validated alongside covered_scripts.

---

Nitpick comments:
In `@spec/noctalia_bar_spec.sh`:
- Around line 8-9: The current awk gsub(/.*id = \"|\";.*/, "") is fragile to
spacing, optional semicolons, or trailing comments; update the awk command that
extracts widget IDs to use a robust quoted-value extraction (e.g. use match()
and substr() to find the first quoted string after an id token or a regex that
captures id\s*=\s*"([^"]*)") so it correctly handles variants like id="Foo", id
= "Foo"; and trailing comments; keep the surrounding logic (the in_right flag
and the right-section exit) intact so the output still lists the widget ids in
order for the test assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6bcac05b-32f0-4157-9a5b-f6133f74f3bc

📥 Commits

Reviewing files that changed from the base of the PR and between fd88f09 and aff6698.

📒 Files selected for processing (11)
  • .github/workflows/e2e.yml
  • config/hyprland/hyprland.conf
  • config/noctalia/default.nix
  • config/noctalia/lock-before-sleep.sh
  • named-hosts/matic/default.nix
  • spec/coverage_spec.sh
  • spec/hyprland_lid_lock_spec.sh
  • spec/matic_lid_policy_spec.sh
  • spec/matic_pam_fingerprint_spec.sh
  • spec/noctalia_bar_spec.sh
  • spec/noctalia_lock_before_sleep_spec.sh
✅ Files skipped from review due to trivial changes (1)
  • spec/noctalia_lock_before_sleep_spec.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/noctalia/default.nix

Comment thread .github/workflows/e2e.yml
Comment on lines +69 to +70
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For GitHub Actions pull_request events, what does github.sha point to (head SHA vs merge SHA), and which refs are guaranteed to exist for raw.githubusercontent.com downloads?

💡 Result:

For GitHub Actions workflows triggered by pull_request events (excluding pull_request_target), github.sha points to the SHA of the merge commit (a temporary test merge of the PR head into the base branch). This is the last merge commit on the GITHUB_REF branch, which is refs/pull/<PR number>/merge [1][2][3]. The head SHA is available via github.event.pull_request.head.sha, and base SHA via github.event.pull_request.base.sha [4][5]. actions/checkout by default checks out this merge commit [6]. For raw.githubusercontent.com downloads, the following refs are guaranteed to exist for any repository (public or private, as long as the file/blob is accessible via the token/permissions): - refs/heads/<branch> for any branch (e.g., https://raw.githubusercontent.com/user/repo/refs/heads/main/file.txt) - refs/tags/<tag> for any tag - <commit-sha> directly (e.g., https://raw.githubusercontent.com/user/repo/abc123/file.txt) - refs/pull/<PR number>/head pointing to the head commit of the PR (even for fork PRs) [7][8][9] - refs/pull/<PR number>/merge pointing to the test merge commit (only if the PR is mergeable) [4][1][10] These refs allow serving raw files from branches, tags, commits, and PRs via raw.githubusercontent.com [11].

Citations:


🏁 Script executed:

cat -n .github/workflows/e2e.yml | sed -n '65,85p'

Repository: shunkakinoki/dotfiles

Length of output: 1071


🏁 Script executed:

# Check if there are any recent changes or comments about this workflow
git log --oneline -20 .github/workflows/e2e.yml 2>/dev/null | head -10

Repository: shunkakinoki/dotfiles

Length of output: 123


🏁 Script executed:

# Check if there are other uses of github.sha or github.event.pull_request.head.sha in the workflow
rg 'github\.(sha|event\.pull_request)' .github/workflows/e2e.yml

Repository: shunkakinoki/dotfiles

Length of output: 542


This step waits for the wrong ref—ensure the guarded SHA is actually available.

Line 70 checks for refs/pull/<num>/head, but line 82 downloads using ${{ github.sha }}, which on pull_request events points to the merge commit (refs/pull/<num>/merge). These are different refs. The merge ref exists only if the PR is mergeable, so the guard can pass while the SHA used by raw.githubusercontent.com is still unavailable.

Either wait for ${{ github.sha }} availability directly, or fetch using ${{ github.event.pull_request.head.sha }} consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e.yml around lines 69 - 70, The guard loop is checking
the wrong ref (refs/pull/${{ github.event.pull_request.number }}/head) while the
download uses ${{ github.sha }}, which on pull_request events refers to the
merge commit; update the check to wait for the exact SHA you later use—either
test availability of ${{ github.sha }} directly or switch the download to use
${{ github.event.pull_request.head.sha }} so both the guard and fetch use the
same ref; ensure the loop command that currently references refs/pull/... is
replaced to verify the chosen SHA/ref (github.sha or
github.event.pull_request.head.sha) before proceeding to
raw.githubusercontent.com.

Comment on lines +8 to +10
if ! "$NOCTALIA_SHELL" ipc call lockScreen lock; then
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Silent lock failure allows suspend with no log; add a logger call before exit 0.

When lockScreen lock returns non-zero (Noctalia IPC down, daemon crashed mid-session, etc.) the script immediately exits with success. systemd proceeds to suspend an unprotected session, with zero visibility into the failure.

The dual Hyprland bindl reduces the risk but doesn't cover the case where the IPC is broken inside an active session (the bindl would have fired before the hook, and if that failed too, no one will know).

At minimum, journal the failure so it's auditable:

🛡️ Proposed fix: log the failed lock before gracefully exiting
+LOGGER="@logger@"
+
 if ! "$NOCTALIA_SHELL" ipc call lockScreen lock; then
+  "$LOGGER" -t noctalia-lock-before-sleep \
+    "WARNING: lockScreen IPC call failed; suspending without a lock screen"
   exit 0
 fi

Wire @logger@ in the pkgs.replaceVars call in config/noctalia/default.nix:

+logger = "${pkgs.util-linux}/bin/logger";
 noctalia_shell = ...;
 sleep = ...;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/noctalia/lock-before-sleep.sh` around lines 8 - 10, The script
currently exits silently when NOCTALIA_SHELL ipc call lockScreen lock fails;
update the failure branch in lock-before-sleep.sh to call the injected logger
(the `@logger`@ variable provided via pkgs.replaceVars) with a clear message
including the command and its exit status before calling exit 0 so the journal
records the lock failure (refer to NOCTALIA_SHELL and the ipc call "lockScreen
lock" to find the branch to modify).

Comment thread spec/noctalia_bar_spec.sh
# shellcheck disable=SC2329

Describe 'config/noctalia/default.nix bar widgets'
CONFIG="$PWD/config/noctalia/default.nix"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

$PWD-based path may resolve incorrectly depending on invocation context.

CONFIG is assigned at Describe scope using $PWD, which is evaluated when ShellSpec parses/sources the spec file. If ShellSpec is invoked from a directory other than the repository root (e.g., shellspec spec/ from a subdirectory, or via a CI runner with a different $CWD), the path $PWD/config/noctalia/default.nix will be wrong and the awk pipeline will silently produce no output, causing a misleading test failure.

Consider using ShellSpec's %SPECROOT or a relative path anchored to the spec's own location, or add a guard:

🛡️ Proposed guard
+  CONFIG="$PWD/config/noctalia/default.nix"

 It 'places DarkMode immediately after Brightness on the right bar'
+  Skip if "config file not found" [ ! -f "$CONFIG" ]
   When run bash -c "awk ..."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/noctalia_bar_spec.sh` at line 5, CONFIG is currently set using $PWD
which can be wrong depending on how ShellSpec is invoked; change the assignment
to use ShellSpec's %SPECROOT (e.g.
CONFIG="%SPECROOT/config/noctalia/default.nix") or compute a path anchored to
the spec file (using dirname on $BASH_SOURCE or $SPEC) so it always resolves
relative to the spec, and add a guard immediately after (e.g. test -r "$CONFIG"
|| shellspec_fail "config not found: $CONFIG") so the spec fails clearly instead
of letting the downstream awk pipeline produce no output.

@shunkakinoki
shunkakinoki merged commit 227559a into main May 7, 2026
31 of 32 checks passed
@shunkakinoki
shunkakinoki deleted the fix/noctalia-lid-lock branch May 7, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant