feat(power): clamshell — ignore lid suspend when docked (#86)
Some checks failed
Check / eval (push) Has been cancelled
Some checks failed
Check / eval (push) Has been cancelled
Explicit HandleLidSwitchDocked=ignore under services.logind so closing the lid with an external display connected does not suspend. Undocked lid path left to upstream defaults. checks.clamshell-logind asserts the baked logind.conf and active logind; real multi-monitor lid V3 queued. Verified: V2 — flake check --no-build; checks.clamshell-logind green. V3 pending: HARDWARE-QUEUE #86 lid+external on a laptop.
This commit is contained in:
@@ -284,7 +284,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see
|
||||
| `nomarchy.system.snapper.enable` | `false` | Hourly/daily BTRFS timeline snapshots + `nixos-rebuild-snap` (installer enables it; no-op unless root is BTRFS) |
|
||||
| `nomarchy.system.power.enable` | `true` | Active power management (see below) |
|
||||
| `nomarchy.system.power.backend` | `"ppd"` | `"ppd"` (power-profiles-daemon + menu/Waybar switcher) or `"tlp"` (deeper battery tuning, no switcher) — mutually exclusive |
|
||||
| `nomarchy.system.power.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present |
|
||||
| `nomarchy.system.power.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present. With power management on, clamshell is also configured: lid close ignores suspend when logind considers the machine docked (external display) — `HandleLidSwitchDocked=ignore` |
|
||||
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
|
||||
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
|
||||
| `nomarchy.hardware.intel.enable` | `false` | Intel enablement above nixos-hardware (GuC/HuC firmware via `i915.enable_guc=3` — `.guc` toggles just that; the installer unsets it on `xe`-driver GPUs); the installer sets it on an Intel CPU/GPU |
|
||||
|
||||
@@ -55,13 +55,6 @@ runtime-computed `resume_offset` into the boot cmdline, and the dev box runs
|
||||
exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in
|
||||
HARDWARE-QUEUE. Close #76 on PASS.
|
||||
|
||||
### 86. Clamshell dock: lid + external monitor (`VISION § C`)
|
||||
Lid closed with external monitor connected must not suspend
|
||||
(`logind` `lidSwitchDocked` / `ExternalPower` audit + display-profile
|
||||
interplay); wake behaves. Display profiles + auto-switch already exist —
|
||||
this is the lid/suspend edge case. VM where possible; residual V3 on
|
||||
real dock hardware.
|
||||
|
||||
### 87. Dock audio sink follows plug/unplug (`VISION § C`)
|
||||
Default audio sink: dock sink on plug, internal on unplug (WirePlumber
|
||||
policy or udev hook). Complements #86; can ship independently.
|
||||
@@ -118,8 +111,8 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
||||
2026-07-10). Keep #59 commented install guidance; no
|
||||
`nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue.
|
||||
|
||||
_(#80–#83 + #85 shipped 2026-07-11. #84 split → **NEXT #86–#87** dock
|
||||
clamshell + dock audio.)_
|
||||
_(#80–#83 + #85–#86 shipped 2026-07-11. Remaining dock slice: **#87**
|
||||
dock audio. #86 V3 clamshell on real external display → HARDWARE-QUEUE.)_
|
||||
|
||||
### v1.0 pointer
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@ the **T14s** (webcam case).
|
||||
|
||||
## Any machine (dev box is fine)
|
||||
|
||||
- [ ] **#86 clamshell lid + external display** — V2 asserts
|
||||
`HandleLidSwitchDocked=ignore` in logind.conf. On a laptop with a
|
||||
real external monitor: open a session, plug external, close lid —
|
||||
machine must **stay awake** (no suspend); external panel remains
|
||||
usable (or wakes with keyboard if DPMS). Unplug external, close lid
|
||||
— should suspend as usual. Optional: with a docked display-profile
|
||||
(`eDP-1` disable), confirm clamshell layout still looks right after
|
||||
lid close. Pass = no surprise suspend when docked.
|
||||
- [ ] **#55 fingerprint enroll on real reader** — with
|
||||
`nomarchy.hardware.fingerprint.enable` and a physical reader: System ›
|
||||
Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional
|
||||
|
||||
@@ -19,6 +19,18 @@ Template:
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-11 — #86 clamshell HandleLidSwitchDocked
|
||||
- **Task:** NEXT #86 — lid closed + external monitor must not suspend.
|
||||
- **Did:** Explicit `HandleLidSwitchDocked=ignore` in
|
||||
`modules/nixos/power.nix` (logind settings.Login). Undocked lid
|
||||
behaviour unchanged. idle.nix comment corrected. `checks.clamshell-logind`
|
||||
asserts conf + active logind. V3 steps in HARDWARE-QUEUE. Closed #86.
|
||||
- **Verified:** **V2** — `nix flake check --no-build`;
|
||||
`checks.clamshell-logind` green (`HandleLidSwitchDocked=ignore` +
|
||||
logind active).
|
||||
- **Pending:** V3 real lid+external on laptop (HARDWARE-QUEUE).
|
||||
- **Next suggestion:** #87 dock audio sink follow.
|
||||
|
||||
## 2026-07-11 — #85 hardware hall of fame + split #84
|
||||
- **Task:** NEXT #85 docs hall of fame; #84 was `[big]` → split.
|
||||
- **Did:** HARDWARE.md §9 install-tested table (T14s Gen 4 AMD, Latitude
|
||||
|
||||
@@ -389,6 +389,11 @@ Design/decision records and a running log of shipped work (items marked
|
||||
✓ **Hardware hall of fame (#85, `VISION § F`):** HARDWARE.md §9
|
||||
install-tested table (T14s Gen 4, Latitude 5410/5310) + “send your DMI
|
||||
line” PR invite; §10 points contributors at the table.
|
||||
✓ **Clamshell logind (#86, `VISION § C`):** explicit
|
||||
`services.logind.settings.Login.HandleLidSwitchDocked = "ignore"` when
|
||||
power management is on (systemd docked = external display present).
|
||||
Undocked lid suspend left to upstream. `checks.clamshell-logind` asserts
|
||||
logind.conf + active logind. Real lid+external V3 in HARDWARE-QUEUE.
|
||||
- ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS
|
||||
passphrase already gates the machine, but locking hyprlock before sleep
|
||||
meant a second password. Fixed by *not locking* before an encrypted
|
||||
|
||||
@@ -80,7 +80,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §11 (doctor).
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Dock life** | Lid closed + external only, wake, default audio sink on undock | Display profiles exist — polish edge cases |
|
||||
| **Dock life** | Lid closed + external only, wake, default audio sink on undock | ✓ #86 clamshell logind ignore when docked; #87 audio still open |
|
||||
| **Hibernate/sleep confidence** | Doctor: resume device, swap size, clean suspend journal | Trust > new power UI |
|
||||
| **Battery health readout** ✓ | Cycles + design capacity % in doctor where sysfs allows | ✓ shipped #80 (report-only; charge limit is a separate row) |
|
||||
| **Charge-limit instant apply** | Already PROPOSED `[blocked:hw]` | Keep privilege tradeoff explicit |
|
||||
|
||||
27
flake.nix
27
flake.nix
@@ -510,6 +510,33 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# Clamshell / dock lid policy (#86): logind must ignore lid close
|
||||
# when docked (external display present). Real multi-monitor lid
|
||||
# events stay V3; this asserts the baked logind.conf + that
|
||||
# logind is alive to consume it. Imports options + power only.
|
||||
clamshell-logind = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-clamshell-logind";
|
||||
nodes.machine = { ... }: {
|
||||
imports = [ ./modules/nixos/options.nix ./modules/nixos/power.nix ];
|
||||
nomarchy.system.power = {
|
||||
enable = true;
|
||||
laptop = true;
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_for_unit("systemd-logind.service")
|
||||
conf = machine.succeed("cat /etc/systemd/logind.conf")
|
||||
assert "HandleLidSwitchDocked=ignore" in conf.replace(" ", ""), (
|
||||
f"clamshell: logind.conf missing HandleLidSwitchDocked=ignore:\n{conf}"
|
||||
)
|
||||
# logind must be running (policy is dead if the unit is down).
|
||||
machine.succeed("systemctl is-active systemd-logind.service")
|
||||
# busctl property: Docked lid policy is not always exported;
|
||||
# re-read conf after logind start is enough for the contract.
|
||||
'';
|
||||
};
|
||||
|
||||
# Runtime VM check for the battery-charge-limit re-apply: the udev
|
||||
# rule must restart the oneshot on an AC state change (firmware can
|
||||
# clear the threshold on unplug). The `test_power` module fakes a
|
||||
|
||||
@@ -85,8 +85,9 @@ in
|
||||
# Suspend only on battery, and sooner than the old fixed 30 min
|
||||
# (it now only fires unplugged). Plugged in, the machine stays
|
||||
# up — long builds, media, presentations aren't killed mid-idle.
|
||||
# Closing the lid still suspends on AC (logind's default): that's
|
||||
# an explicit "I'm done", distinct from sitting idle.
|
||||
# Lid close is logind's job (not hypridle): undocked lid still
|
||||
# suspends (HandleLidSwitch); docked/clamshell lid is ignore
|
||||
# (HandleLidSwitchDocked — modules/nixos/power.nix, #86).
|
||||
{ timeout = 900; on-timeout = "${onAc} || systemctl suspend"; }
|
||||
];
|
||||
};
|
||||
|
||||
@@ -31,6 +31,20 @@ in
|
||||
services.power-profiles-daemon.enable = lib.mkDefault ppd;
|
||||
services.tlp.enable = lib.mkDefault tlp;
|
||||
|
||||
# Clamshell / dock (BACKLOG #86): when the machine is "docked" in
|
||||
# logind's sense (≥1 external display connected), closing the lid
|
||||
# must NOT suspend — the external panel is the session. systemd's
|
||||
# built-in default is already ignore; we set it explicitly so a
|
||||
# downstream override or lock-bump drift is visible, and so
|
||||
# checks.clamshell-logind can assert the contract.
|
||||
# Lid alone (undocked) keeps the normal suspend path
|
||||
# (HandleLidSwitch / ExternalPower left to upstream defaults).
|
||||
# Display-profile "docked" layouts (eDP off) are orthogonal and
|
||||
# already handled by nomarchy.displayProfiles.
|
||||
services.logind.settings.Login = {
|
||||
HandleLidSwitchDocked = lib.mkDefault "ignore";
|
||||
};
|
||||
|
||||
# thermald is Intel-only, so off unless asked (the installer enables
|
||||
# it on a GenuineIntel CPU). Sits happily next to either backend.
|
||||
services.thermald.enable = lib.mkDefault cfg.thermal.enable;
|
||||
|
||||
Reference in New Issue
Block a user