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.
386 lines
30 KiB
Markdown
386 lines
30 KiB
Markdown
# 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.
|