Files
mesh-catalog/modules/asus-zephyrus-g14/README.md
T
jochen 1d4de00603 i3: other modules' lines are contributions to node-display-session (hq ADR 0212)
rofi, clipmenu, feh, i3status-rust and the laptop's model module wrote files into i3's config.d,
naming no dependency on the window manager. They now contribute their lines; i3 places them under a
line naming each module, and config.d is the operator's alone. The catalogue-wide test composes the
contributions as the controller does and checks the whole with i3 -C.
2026-10-05 10:11:27 +02:00

386 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# asus-zephyrus-g14
The hardware module for the **ASUS ROG Zephyrus G14** laptop: its vendor daemon and platform
profiles, the hybrid GPU's mode and driver options, suspend, the lid and power key, low battery,
the backlights, the vendor keys and the touchpad (novox/hq research 027/03 *Power management on
the laptop*, research 026/05, to-be 42 phase 3).
## Why this name
A module is named after the hardware model, never the node (novox/hq ADR 0112; research 026/03:
no flavors, no machine names). `asus-zephyrus-g14` is the model family exactly as the firmware
reports it (`/sys/class/dmi/id/product_family` = `ROG Zephyrus G14`). The module's code checks that
value and its switcher does nothing on any other model, and `zephyrus_check` reports it.
A wider name such as `asus-rog-laptop` would promise what this module cannot keep. Its contents
belong to this family: the vendor-key scan codes, the eDP panel beside an NVIDIA dGPU, and the NVIDIA
D3 workaround. A second G14 is assigned the same module. Another ROG model gets its own.
Written against the GA403 (2024, Ryzen 8945HS, RTX 4070 Laptop, hybrid). Older G14 years have the same
daemons and probably the same keys. Their GPU options are unverified.
## What it owns
| | what | how |
|---|---|---|
| package | `asusctl` (asusd + client) | the distribution's package (`extra`). The machine was found with a local build of 6.4.0. The host only asserts *present*, so the switch to 6.5.0 from `extra` happens at the next `pacman -Syu` (or `pacman -S asusctl`). `zephyrus_check` flags a local build |
| package | `upower`, `playerctl` | what the low-battery drop-in and the media keys use. `xinput` is the `xorg` module's (one package, one module on a node) |
| service | `asusd` running (static unit: no boot state to declare), `supergfxd` running and enabled | |
| archive | `/usr/local/lib/asus-zephyrus-g14/bin/` | the module's scripts, from `files/bin` (below) |
| file ×2 | `~/.config/i3/config.d/10-asus.conf`, `20-g14.conf` | the laptop's lines in i3: the keys the firmware sends as ordinary presses (Fn+F6, Fn+F9), the keyboard-backlight notifier, the panel as primary, the touchpad key. The paths are adopted, because a second file binding the same keys makes i3's configuration check fail, and the `i3` module's watcher then reloads nothing |
| file ×4 | `asus-zephyrus-g14-touchpad-resume.service`, and a drop-in `asus-zephyrus-g14-touchpad.conf` on each sleep service | the touchpad's settings once more after a resume (below) |
| file | `/etc/modprobe.d/g14-nvidia-power.conf` | `NVreg_DynamicPowerManagement=0x00` (runtime D3 off: the ACPI D-Notifier hang) and `NVreg_PreserveVideoMemoryAllocations=1`. The path is adopted (ADR 0182) |
| file | `/etc/modprobe.d/video-brightness-switch.conf` | `video.brightness_switch_enabled=0`, so the ACPI video driver does not also move a backlight on the keys. The file was on the machine and owned by nothing |
| file ×3 | `systemd-{suspend,hibernate,suspend-then-hibernate}.service.d/asus-zephyrus-g14-nvidia.conf` | `Wants=` the matching `nvidia-*` sleep units and `nvidia-resume` (see *suspend units* below) |
| file | `nvidia-powerd.service.d/asus-zephyrus-g14.conf` | `ConditionKernelCommandLine=zephyrus.nvidia-powerd`: Dynamic Boost runs only when the operator opts in at boot |
| file | `/etc/systemd/logind.conf.d/power.conf` | the power key and the lid suspend, on battery, on mains and docked. `systemd-logind` is reloaded, never restarted |
| file | `/etc/udev/rules.d/90-backlight.rules` | backlights writable by the `video` group. `systemd-udevd` is reloaded |
| file | `triggerhappy.service.d/asus-zephyrus-g14.conf` | `thd … --user ${machine:account}`: the triggers run as the operator's account (below) |
| file | `/etc/triggerhappy/triggers.d/asus-g14.conf` | the vendor keys: media (`KEY_PROG1/3/4`), panel brightness, touchpad (`KEY_F21`). The path is adopted, because two trigger files would fire every key twice |
| file | `/etc/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf` | low battery at 15/10/7 %; at 7 % **suspend**, not power off. A drop-in over the package's own file |
| file | `/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf` | tap to click, natural scrolling, acceleration 0.15, as an X input class |
**What it does not own, on purpose:**
- `/etc/asusd/*.ron` belong to asusd, which rewrites them whenever a setting changes. RON is not a
format the host writes into (ADR 0102 speaks JSON and marked blocks). Owning the file whole would
repeat the predecessor's freeze: the measured file already differs from the one the predecessor
shipped. The settings the module needs are set through asusd, by its code (below).
- `/etc/supergfxd.conf` and `/etc/modprobe.d/supergfxd.conf` belong to supergfxd, which writes both.
- The swap file, its unit and the swap partition are the machine's swap layout (research 027,
question 3). They are not this module's, nor `memory-pressure`'s.
- **Places.** The screen layouts for named places (`$mod+Alt+1…7`, `~/.screenlayout/`,
`~/scripts/.screenlayouts/@*.sh`) name where the operator works, which no module may (ADR 0112).
They are the operator's own lines until autorandr profiles replace them (the `xorg` module).
- **The monitor-hotplug wizard** (`/etc/udev/rules.d/91-monitor-hotplug.rules`,
`~/scripts/.screenlayouts/monitor-wizard.sh`) is any laptop's, not this model's. The predecessor said
so itself (its `laptop` flavor). It is the display server's to replace with autorandr's own hotplug
handling. Until then it stays as found, and it still calls the predecessor's `as-user`.
- **The screenshot script** (`~/.config/i3/scripts/screenshot.sh`) is any machine's. The `i3` module
binds it too. This module only binds the key the firmware sends for it.
- **The bar's battery block.** It belongs here (a block that follows this model's hardware), but the
bar has no way in yet (`i3status-rust` README). Once ADR 0210's contributions reach the bar seat,
this module contributes it.
**Requires `x11-display`.** The i3 lines and the session scripts need a display, so the module is
assigned where the display server is.
## Software outside the distribution (ADR 0205, research 027 question 1)
`supergfxctl` (5.2.7, from the asus-linux repository, which is no longer configured) and
`triggerhappy` (AUR) are **kept as found, and depended on**. The module declares no package for
either, because the host installs from the official repositories only. It declares their services
(`supergfxd` running, `triggerhappy` running), so on a machine without them the host refuses the
service by name: *does not exist on this machine*. The refusal is loud, never a silent pass.
`zephyrus_check` names both as foreign.
This module does not choose between the options of research 027 question 1. Under the starting
position (P2: the build machine builds AUR packages into a repository the mesh serves), both become
`package` resources here, and a fresh G14 installs them. **Until P2 exists, a fresh G14 is blocked
on installing these two by hand.** ADR 0205's vendored archive (P1) does not fit: supergfxctl is a
daemon with a system-bus policy and udev rules, and triggerhappy is C.
A later option for the keys: the module's own Go code could read the vendor keys from evdev, which
the operator's account may do through the `input` group. That would retire triggerhappy entirely.
It is not done here, because it would put the keys behind the node's runtime, and the runtime
restarts a bundle that dies only on its next call (below).
## The long-running code: the profile switcher (ADR 0198)
The module's Go bundle serves the tools and runs the platform-profile switcher in the same process.
The node's runtime launches the bundle at the runtime's start. It replaces the predecessor's
`auto-profile`, a user unit that woke every five seconds, on battery too.
- **Policy** (constants until settings exist, issue 168): battery → `Quiet`; mains → `Balanced`;
mains with the CPU at or above 50 % for 3 samples of 10 s → `Performance`, back to `Balanced` after
3 samples at or below 20 %. Between the lines nothing moves (hysteresis). iowait counts as idle.
- **Woken by events, not a poll.** The kernel's power-supply uevents (netlink, group 1) wake the
switcher. Any account may listen on that group, and it needs no daemon, bus client or dependency;
upower re-announces the same changes but would need a D-Bus client in the bundle. The CPU is
sampled only on mains, every 10 s, because only there does the answer depend on it. On battery,
a safety re-read every 5 minutes covers an event lost across a suspend. If the uevent socket
cannot be opened, the switcher polls every 10 s and says so in `zephyrus_profile_policy`.
- **The battery decides the source.** A battery that is *discharging* means battery, whatever any
adapter says. The predecessor took any `online` file reading 1 as mains, and on this model the USB-C
ports report `online`. Batteries of `scope=Device` (a mouse, a headset) are ignored.
- **It acts on a change of its decision, never to restore one.** A profile someone chose by hand (the
profile key, asusctl, `zephyrus_profile`) stays until the power source changes or the load crosses
a line. The predecessor re-asserted its choice every five seconds, which made the profile key
useless. **Starting is not a decision**: the runtime restarts the bundle on every push that changes
one, and a push must not reset the operator's profile.
- **A hold.** `zephyrus_profile` holds the profile it sets for 60 min (`hold_minutes`). A change of
power source ends the hold.
- **One assertion at start:** through asusctl, the charge limit (80 %) and asusd's own on-mains and
on-battery profiles (`Balanced`, `Quiet`), each read first and set only if it differs. asusd's own
switching on a change of power source then agrees with the switcher's. A limit set later with
`zephyrus_charge_limit` stands until the bundle next starts. For a one-off full charge, use its
`oneshot`.
- **Events:** `profile.switched` (`profile`, `from`, `reason`, `source`), published through the
runtime.
No root is involved. asusd's and supergfxd's bus policies admit the `users` and `wheel` groups, and the
runtime runs as the operator's account. The one write that may escalate is the panel's backlight,
when the udev rule has not run yet. It uses `sudo -n` and never prompts. Every command is bounded at
20 s.
**Known limit.** The runtime restarts a launched bundle that exits *on its next tool call*, not at
once (mesh-tools `launch.ts`), so a crashed switcher stays down until a tool is called. ADR 0198 §1
says *started again when it exits*. The switcher recovers from a panic and reports it in
`zephyrus_profile_policy` and `zephyrus_check`, but a crash of the process is the runtime's to restart.
## The vendor keys and the scripts
triggerhappy opens the input devices as root, then **drops to the operator's account with its groups**
(`initgroups`: `input`, `video`). The packaged unit already drops to `nobody`, and the module's drop-in
names the account instead. The predecessor replaced the packaged unit with one that ran every trigger
as root, then `su`-ed to a named person with a hard-coded uid and display, and sourced a file of
secrets on the way (research 027 question 2). Now:
- `zephyrus-session CMD…`: runs a command in the account's graphical session. It sets the account's
own bus (`/run/user/<uid>/bus`) and takes the display and its authority from the session's window
manager's own environment, as the desktop modules' session finder does. If i3 is not running, it
asks logind, and then any process of the account that has a display. Nothing is sourced.
triggerhappy's `--user` changes the user and its groups and nothing else: the triggers start with
the service's bare environment, which is why every key that needs the session goes through this.
- `zephyrus-media play-pause | next | previous`: the media keys through MPRIS, with a notification of
what happened and a lock against the key's own repeat.
- `zephyrus-display primary | order`: the internal panel as the primary output, and the display key's
workspace split (odd workspaces on the panel, even ones on the first external output). The panel is
found as the connected `eDP` output. The predecessor named `eDP-1` and used `jq`. This reads i3's
answer without it.
- `zephyrus-kbd-notify`: the keyboard backlight's level, shown when UPower says it changed. One
instance per session, because i3 runs its `exec` lines again on an in-place restart.
- `zephyrus-backlight + | - | N`: the panel in 5 % steps, never below 1 %. **The panel is the
backlight under the eDP connector**, because this model also registers `nvidia_0`, which moves
nothing. The predecessor named `amdgpu_bl1` literally.
- `zephyrus-notify ID TEXT`: one replacing notification, through `busctl` (the service manager's
client, so no libnotify).
- `zephyrus-touchpad reset | toggle`: bound to the touchpad key (`KEY_F21`).
**Media keys** go to MPRIS through `playerctl`. The predecessor's fallback to a media server's local
API needed a token from the secrets file, and is dropped until a module can be handed a secret
(research 027 question 2). A player that does not speak MPRIS is told as "Media: no player".
## The touchpad: an input class instead of a sleep hook
The predecessor re-ran `xinput` from `/etc/systemd/system-sleep/` after every resume, as a named person
on a guessed display, because settings made with `xinput` are lost when the device initialises again.
An X input class is applied by X **every time the device appears**: at login, on hotplug and after a
resume. So the cause is fixed. The class matches any touchpad on the machine, which is the model's, so it
holds across G14 years whose touchpads differ. It takes effect at the next X start.
`zephyrus-touchpad reset` stays as the manual form, on the touchpad key and `$mod+Shift+x`.
**And a backstop after resume.** A resume that does not initialise the device again does not make X
apply the class either, and the predecessor's i3 file says the touchpad "sometimes needs re-init after
sleep". So `asus-zephyrus-g14-touchpad-resume.service` runs `zephyrus-touchpad reset` as the account,
two seconds after the machine is awake. It is never enabled. Each sleep service `Wants=` it through a
drop-in, and it is ordered `After=` them, which is how `nvidia-resume` is started too.
`zephyrus_check` says whether the sleep wants it.
## Suspend units without enabling them
`nvidia-suspend`, `-hibernate`, `-suspend-then-hibernate` and `-resume` are enabled with links in the
sleep services' `.wants` directories. The mesh makes no links (ADR 0012). The host's service shape
cannot declare them either: it may only say *running* or *stopped*, and *running* on a one-shot that
last failed would start `nvidia-sleep.sh suspend` with the machine awake. So the module asks for them
from the other side: a drop-in on each sleep service that `Wants=` them. The units' own
`Before=`/`After=` order them. The found links stay and are harmless.
`suspend-then-hibernate` now also gets `nvidia-suspend-then-hibernate`, which the machine lacked.
The drop-ins take effect at the service manager's next `daemon-reload`. In the same apply, the restart
of `triggerhappy` (whose drop-in changes) performs one.
## Tools
| tool | r/a | what |
|---|---|---|
| `zephyrus_brightness` | r/a | panel (percent or ±step, floor 1 %) and keyboard (off/low/med/high, 0-3, ±) through asusd |
| `zephyrus_battery` | r | charge, energy in Wh, health (full ÷ design), cycles (the firmware reports 0, and this is said), limit, watts, hours left |
| `zephyrus_charge_limit` | r/a | 20-100 through asusd; `oneshot` |
| `zephyrus_gpu_mode` | r/a | mode, supported modes, dGPU power, the pending mode and action; says that asusd switches the mode on every change of power source |
| `zephyrus_profile` | r/a | active, on-mains and on-battery profile, kernel platform profile; set with a hold |
| `zephyrus_thermals` | r | every hwmon temperature and fan, the hottest, the dGPU's temperature **only when it is awake** (nvidia-smi wakes a suspended GPU) |
| `zephyrus_power_draw` | r | battery flow, APU package power (PPT), dGPU draw when awake, power source and why |
| `zephyrus_profile_policy` | r | what the switcher would choose now and why: source, recent load against the thresholds, decision, hold, last switch, what woke it, what it asserted at start, and whether the predecessor's switcher still runs |
| `zephyrus_fan_curves` | r | asusd's curves per profile and fan |
| `zephyrus_keys` | r | every custom key: each triggerhappy trigger, the module's i3 lines and the keys the firmware handles; what runs and the file it is in. Warns when one key is in two trigger files, which fires it twice |
| `zephyrus_check` | r | every expectation: model, packages (local or foreign), daemons, nvidia-powerd, sleep units, the NVIDIA options **in force** (`/proc/driver/nvidia/params`), charge limit, one authority each over the profile and the GPU mode, predecessor leftovers; the touchpad resume unit wanted by the sleep; triggerhappy running as the operator's account; any trigger, udev rule or sleep hook still naming `as-user`. It also lists what it did not check |
`profile` is a candidate verb for a future `node-power-profile` seat (research 027/03). That seat has
no record yet, so this is the module's own tool.
## Found on the laptop, 2026-10-04 (read-only)
- **Two authorities over the GPU mode.** `asusd.ron` has `ac_command: "supergfxctl -m Hybrid"` and
`bat_command: "supergfxctl -m Integrated"`, so asusd switches the GPU mode on every change of power
source. **supergfxd 5.2.7 cannot read logind's sessions** (`manager is an invalid variant`, every
boot), so a switch that needs a logout times out. `zephyrus_check` reports both. The fix is the
operator's, in asusd's file: clear both commands, or update supergfxctl once it can be packaged.
- **`brightness.conf` did nothing.** `HandleBrightnessKey` is not a logind key, and logind logs
*Unknown key … ignoring* at every start. The module does not carry it. The brightness keys were
always triggerhappy's, with the ACPI video switch off.
- **Two profile switchers** would run at once until `auto-profile` is stopped (below).
- **asusctl is a local build** (6.4.0, *Unknown Packager*) beside a foreign `asusctl-debug`.
## When assigned to the laptop: what changes
1. `/usr/local/lib/asus-zephyrus-g14/` appears (seven scripts).
2. Written over found files (each original kept once by the host): `g14-nvidia-power.conf` and
`video-brightness-switch.conf` (same options, so no change until the next boot either),
`logind.conf.d/power.conf` (same keys; logind reloaded), `90-backlight.rules` (same effect;
udevd reloaded), `triggers.d/asus-g14.conf` (now the module's scripts), and i3's `10-asus.conf` and
`20-g14.conf` (the module's lines; the `i3` module's watcher checks and reloads them).
3. New: the three sleep drop-ins (behaviour gained: `nvidia-suspend-then-hibernate`), the
nvidia-powerd drop-in (no effect while it is masked), the triggerhappy drop-in, the UPower drop-in
(same values as today), the touchpad input class (at the next X start), and the resume unit with
its three drop-ins.
4. `daemon-reload` and a `triggerhappy` restart. thd now runs as the account and the keys run the
module's scripts. `upower` restarts.
5. Packages, asusd and supergfxd: already as declared, so nothing changes. asusctl stays the local
6.4.0 until the next upgrade.
6. The node runtime restarts with the new bundle. The switcher asserts the limit (80, already) and
asusd's profiles (Balanced and Quiet, already), so it sets nothing. It takes the current decision
as applied and acts from the first event on.
## Predecessor files this module makes redundant — the operator removes them once (ADR 0182)
**On the laptop:**
1. `systemctl --user disable --now auto-profile.service`, then delete
`~/.config/systemd/user/auto-profile.service` and `~/scripts/auto-profile`. **Do this right after
the push**, or two switchers run at once.
2. **Before the push, keep the place layouts.** The module writes `20-g14.conf` over the found file,
and the found file carries the seven `$mod+Alt+1…7` layout bindings, which name places. Move them,
and the `exec … ~/.screenlayout/@default.sh` line, to a file of your own in the same directory,
e.g. `~/.config/i3/config.d/90-layouts.conf`. i3 reads it the same way, and it stays yours until
autorandr profiles replace it. The host keeps the found `20-g14.conf` once in any case.
3. `~/scripts/asus-bright`, `~/scripts/asusctl-kbd-bright`, `~/scripts/xrandr-bright`,
`~/scripts/media-control`, `~/scripts/xinput-reset-touchpad`,
`~/scripts/.screenlayouts/orden-workspaces.sh` and `~/.config/i3/scripts/kbd-brightness-notify.sh`:
no trigger and no line of the module's uses them any more.
**Keep `~/scripts/as-user`** while `/etc/udev/rules.d/91-monitor-hotplug.rules` exists: that rule
still runs the monitor wizard through it. `zephyrus_check` names every place that still calls it.
4. `/etc/systemd/system/triggerhappy.service`: the predecessor's replacement of the packaged unit. The
module's drop-in works over either, so delete it and `systemctl daemon-reload` to return to the
packaged unit (`Type=notify`, socket).
5. `/etc/systemd/logind.conf.d/brightness.conf`: the unknown key, which does nothing.
6. `/etc/systemd/system-sleep/xinput-reset-touchpad.sh`, if it is still there: replaced by the input
class and the resume unit. (It was already gone on 2026-10-04.)
7. `/etc/UPower/UPower.conf`: the predecessor's replacement of the package's file. Its values are now
the module's drop-in. Restore the package's copy (`rm` it, then `pacman -S upower`).
8. Optional: `systemctl disable nvidia-suspend nvidia-resume nvidia-hibernate` (the drop-ins carry them
now), `/etc/asusd/*.ron-old` and `fan_curves.ron.bak`, the foreign `asusctl-debug` package, and
`pacman -S asusctl` for the distribution's build.
**Stays the machine's:** `/swapfile` and `/etc/systemd/system/swapfile.swap` (the swap layout),
`/etc/udev/rules.d/91-monitor-hotplug.rules` (the display's, phase 2), and the place layouts.
**On the desktop** (the predecessor's G14 flavor reached it; part was removed on 2026-10-04): none of
this module applies there. Still present and to be deleted:
`/etc/systemd/logind.conf.d/brightness.conf`, `/etc/systemd/system-sleep/xinput-reset-touchpad.sh`,
`~/scripts/xinput-reset-touchpad`, `~/scripts/xrandr-bright` and
`~/.config/i3/scripts/kbd-brightness-notify.sh`.
## The predecessor, file by file
The predecessor carried this model in its desktop module's `g14` and `laptop` flavors, and in a
`g14-power` module. Every model-specific file of the desktop module, and where it is now:
| predecessor file | now |
|---|---|
| `i3-asus.conf` → `config.d/10-asus.conf` | **this module**, the same path; the scripts it calls are the module's |
| `i3-g14.conf` → `config.d/20-g14.conf` | **this module**, the same path. The place layouts in it are the operator's own file (migration, step 2) |
| `g14-triggerhappy-asus-g14.conf` | **this module**, the same path; the triggers run the module's scripts |
| `g14-triggerhappy.service` (a replacement of the packaged unit) | **retired**: a drop-in over the packaged unit (`--user`) |
| `as-user` | **retired**: thd runs as the account, and `zephyrus-session` finds the session. Kept on the machine while the monitor wizard's udev rule calls it |
| `asus-bright` | **this module**: `zephyrus-backlight`, the panel found by its connector |
| `media-control` (bound by the `g14` triggers) | **this module**: `zephyrus-media`, without the fallback that needed a secret |
| `xinput-reset-touchpad` | **this module**: the input class, `zephyrus-touchpad`, and the resume unit |
| `g14-system-sleep-xinput-reset-touchpad.sh` | **this module**: the input class, and the resume unit that the sleep services want |
| `kbd-brightness-notify.sh` | **this module**: `zephyrus-kbd-notify`, one instance per session |
| `asusctl-kbd-bright` | **retired**: the keyboard keys are the firmware's, and `zephyrus_brightness` sets it by hand |
| `xrandr-bright` | **retired**: a software dimming; the panel's backlight is `zephyrus-backlight` |
| `screenlayout-orden-workspaces.sh` | **this module**: `zephyrus-display order`, on Fn+F9 |
| `screenlayout-*.sh` (six named places) | **the operator's**, until autorandr profiles (the `xorg` module) replace them |
| `g14-90-backlight.rules` | **this module**, the same path |
| `g14-logind-brightness.conf` | **retired**: an unknown logind key that did nothing |
| `laptop-monitor-wizard.sh`, `laptop-91-monitor-hotplug.rules` | **any laptop's, not this module's**: kept as found, for the `xorg` module's autorandr to replace |
| `bottom-bar.g14.toml` (the battery block) | **waits**: this module's, once the bar takes contributions (ADR 0210) |
| `razer-basilisk-battery-percentage` | **not this model's**: a mouse. No module carries it (`i3status-rust` README) |
| `screenshot.sh` | **not this model's**: any machine's script, which the `i3` module binds too; this module binds Fn+F6 to it |
| package `triggerhappy` | **depended on, kept as found** (outside the distribution, above) |
`g14-power`'s pieces (the NVIDIA options, logind, UPower, the sleep units, `auto-profile`) are in
*What it owns* and the switcher above. Its memory guard is the `memory-pressure` module's.
## What the predecessor paid for, and what this module does about it
- **One file for every machine overwrote what each machine needed** (the predecessor's split of its i3
configuration into a base, an ASUS and a G14 layer, 2026-03). Here the model's lines are this
module's files, and nothing else writes them.
- **The layers silently stopped applying.** The predecessor's chain *laptop → g14* was stated in two
places, and the one its installer read lacked it. For weeks the G14 received only its 21
G14-tagged files, and the 49 base and laptop files were never seeded. Validation and the pipeline
both reported success (2026-08, found at a cutover). Here a module is one manifest. Its tests assert
that every trigger runs a script it ships, and `zephyrus_check` and `zephyrus_keys` read back what is
on the machine.
- **A hook wrote asusd's own files with sudo,** because configuration sync might not run on install
(2026-04), and froze them. asusd rewrites those files whenever a setting changes. Here the module
sets asusd through its client and never writes the RON files.
- **A profile daemon fought the profile key.** The predecessor turned asusd's own switching off and
re-asserted its choice every five seconds (2026-03). Here the switcher acts on a change of its
decision, never to restore one.
- **The overnight freeze** was an ACPI power-source event hanging the NVIDIA GPU in runtime D3
(2026-06). It is fixed by the driver options, which `zephyrus_check` reads from the running driver
rather than from the file.
- **A unit restarted about 73,000 times, unnoticed** (2026-06). Here every expectation is in
`zephyrus_check`, and so is a list of what it did not check.
- **The vendor keys ran as root and `su`-ed to a named person** on a guessed display, sourcing a file
of secrets. Here thd drops to the account, and nothing is sourced.
- **The touchpad lost its settings after a resume,** because `xinput` settings vanish when the device
initialises again. Here an input class re-applies them, with the resume unit as a backstop.
## Tests
`go test ./...` in this directory. Every tool runs against a tree standing in for `/sys`, `/proc` and
`/etc`, and an injected runner answering with what asusctl 6.4 and supergfxctl 5.2 said on the laptop.
The tests cover:
- the power-source rule;
- battery arithmetic from `charge_*`;
- the eDP panel choice;
- brightness bounds;
- the policy's sustain, relax and hysteresis, with iowait counted as idle;
- the switcher: no act at start, one switch per change of source, a published event, boost from
samples, holds, retry after failure, start-up assertions only where they differ, inert on another
model;
- the uevent filter;
- the manifest: tools listed equal tools served, no machine named, triggers exist, every key runs a
shipped executable script, every script passes `bash -n`;
- `zephyrus_keys` over the module's own triggers and i3 lines, and a key in two trigger files;
- the checks for `as-user`, triggerhappy's account and the resume unit.
## The vendor keys are a contribution (changed 2026-10-04, novox/hq ADR 0212)
The trigger file and triggerhappy's service drop-in are no longer this module's. The `triggerhappy`
module holds `node-hotkeys`, owns the daemon, and reads only the mesh's trigger file. This module
contributes its eight trigger lines (media, panel brightness, touchpad) to that seat, so it depends
on a hotkey holder being assigned beside it. Its keys still run this module's own scripts.
`zephyrus_keys` reads the trigger directory as before.
## The touchpad after waking, and the lid, move to the power module (changed 2026-10-04, novox/hq ADR 0211)
The touchpad resume unit and its three drop-ins on the sleep services are gone. The reset is now this
module's contribution to `node-power`'s `after-wake` moment, so the module depends on the power
module. `logind.conf.d/power.conf` is the power module's. This laptop's values (suspend on the power
key and on the lid in every case) are that module's settings for this machine. The NVIDIA driver's
sleep drop-ins stay here: they must run inside the sleep transaction, which a contribution cannot.
## Its i3 lines are a contribution (changed 2026-10-05, novox/hq ADR 0212)
The module no longer writes a file into i3's `config.d`. Its window-manager lines (the source is still
under `files/i3/` where it had one) are a contribution to `node-display-session`. The i3 module places
them in its own configuration under a `# <module>` line, so this module depends on a window manager
being assigned beside it.