diff --git a/modules/asus-zephyrus-g14/README.md b/modules/asus-zephyrus-g14/README.md index e3e17e0..94c55a5 100644 --- a/modules/asus-zephyrus-g14/README.md +++ b/modules/asus-zephyrus-g14/README.md @@ -24,9 +24,11 @@ daemons and probably the same keys. Their GPU options are unverified. | | 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`, `xorg-xinput` | what the low-battery drop-in, the media keys and the touchpad key use | +| 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) | @@ -47,8 +49,21 @@ daemons and probably the same keys. Their GPU options are unverified. - `/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. -- The i3 fragments (`~/.config/i3/config.d/10-asus.conf`, `20-g14.conf`) and the keyboard-backlight - notifier they start belong to phase 2 (the `i3` module). +- **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) @@ -122,8 +137,19 @@ as root, then `su`-ed to a named person with a hard-coded uid and display, and s 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//bus`) and finds the display from logind, or from a process of the account - that has one. Nothing is sourced. + own bus (`/run/user//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. @@ -131,17 +157,25 @@ secrets on the way (research 027 question 2). Now: 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 Plex fallback needed a Plex token -from the secrets file and is dropped until a module can be handed a secret (research 027 question 2). +**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, and the hook is gone. 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. +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 @@ -170,7 +204,8 @@ of `triggerhappy` (whose drop-in changes) performs one. | `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_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. It also lists what it did not check | +| `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. @@ -190,14 +225,16 @@ no record yet, so this is the module's own tool. ## When assigned to the laptop: what changes -1. `/usr/local/lib/asus-zephyrus-g14/` appears (four scripts). +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). + 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), and the touchpad input class (at the next X start). + (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 @@ -213,16 +250,23 @@ no record yet, so this is the module's own tool. 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. `~/scripts/asus-bright`, `~/scripts/asusctl-kbd-bright`, `~/scripts/xrandr-bright`, - `~/scripts/as-user`, `~/scripts/media-control`: no trigger and no i3 binding uses them any more. -3. `~/scripts/xinput-reset-touchpad`: still started and bound by `~/.config/i3/config.d/20-g14.conf`. - Point those two lines at `/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset`, or wait for - the i3 module (phase 2) to rewrite the fragment. +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`: replaced by the input class. +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 @@ -230,7 +274,7 @@ no record yet, so this is the module's own tool. `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 i3 fragments. +`/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: @@ -238,6 +282,65 @@ this module applies there. Still present and to be deleted: `~/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 @@ -254,4 +357,6 @@ The tests cover: 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`. + 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. diff --git a/modules/asus-zephyrus-g14/cmd/zephyrus/check.go b/modules/asus-zephyrus-g14/cmd/zephyrus/check.go index 727c994..4353b14 100644 --- a/modules/asus-zephyrus-g14/cmd/zephyrus/check.go +++ b/modules/asus-zephyrus-g14/cmd/zephyrus/check.go @@ -104,6 +104,18 @@ func (m *Machine) Check(ctx context.Context, sw *Switcher) CheckReport { add("nvidia suspend and resume", strings.Contains(wants, "nvidia-suspend.service") && strings.Contains(wants, "nvidia-resume.service"), "systemd-suspend.service %s", strings.TrimSpace(wants)) + add("touchpad after resume", strings.Contains(wants, "asus-zephyrus-g14-touchpad-resume.service"), + "systemd-suspend.service wants asus-zephyrus-g14-touchpad-resume.service: %v (a daemon-reload makes a new drop-in count)", + strings.Contains(wants, "asus-zephyrus-g14-touchpad-resume.service")) + if uid := m.triggerhappyUID(); uid < 0 { + add("triggerhappy as the account", false, "no thd process runs: the vendor keys do nothing") + } else { + add("triggerhappy as the account", uid == operatorUID(), "thd runs as uid %d; the operator's account is %d (the module's drop-in passes --user)", uid, operatorUID()) + } + refs := m.asUserReferences() + add("no as-user", len(refs) == 0, "%s", orNone(map[bool]string{true: "", false: "the predecessor's as-user is still named in " + strings.Join(refs, ", ") + + ": it su-s to a named person on a guessed display and sources a file of secrets"}[len(refs) == 0])) + params := ParseNvidiaParams(m.read("/proc/driver/nvidia/params")) if len(params) == 0 { add("nvidia options", false, "the NVIDIA driver is not loaded (no /proc/driver/nvidia/params)") @@ -165,6 +177,10 @@ var PredecessorHomeFiles = []string{ "scripts/asus-bright", "scripts/asusctl-kbd-bright", "scripts/xrandr-bright", + "scripts/media-control", + "scripts/xinput-reset-touchpad", + ".config/i3/scripts/kbd-brightness-notify.sh", + "scripts/.screenlayouts/orden-workspaces.sh", } func orNone(s string) string { diff --git a/modules/asus-zephyrus-g14/cmd/zephyrus/keys.go b/modules/asus-zephyrus-g14/cmd/zephyrus/keys.go new file mode 100644 index 0000000..017c46a --- /dev/null +++ b/modules/asus-zephyrus-g14/cmd/zephyrus/keys.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +// Key is one custom key on the laptop: what it is, what runs, and where that is defined. +type Key struct { + Key string `json:"key"` + Physical string `json:"physical,omitempty"` + When string `json:"when,omitempty"` + Runs string `json:"runs"` + From string `json:"from"` +} + +// KeysReport is what zephyrus_keys answers. +type KeysReport struct { + Keys []Key `json:"keys"` + Warnings []string `json:"warnings"` + NotRead []string `json:"not_read"` +} + +// TriggerDir is triggerhappy's directory of trigger files; the module owns one file in it. +const TriggerDir = "/etc/triggerhappy/triggers.d" + +// I3Fragments are the module's own files in i3's include directory, relative to the account's home. +var I3Fragments = []string{".config/i3/config.d/10-asus.conf", ".config/i3/config.d/20-g14.conf"} + +// physicalKeys names the key behind an evdev code, as far as it is known on this model. The media +// codes are the M4 key and Fn+F4/F5 together; which code is which key was not recorded. +var physicalKeys = map[string]string{ + "KEY_PROG1": "a media key (M4, Fn+F4 or Fn+F5)", + "KEY_PROG3": "a media key (M4, Fn+F4 or Fn+F5)", + "KEY_PROG4": "a media key (M4, Fn+F4 or Fn+F5)", + "KEY_BRIGHTNESSDOWN": "Fn+F7", + "KEY_BRIGHTNESSUP": "Fn+F8", + "KEY_F21": "Fn+F10 (touchpad)", + "$mod+Shift+s": "Fn+F6 (screenshot; the firmware sends Super+Shift+S)", + "$mod+p": "Fn+F9 (display; the firmware sends Super+P)", +} + +// firmwareKeys are handled below any configuration file. +var firmwareKeys = []Key{ + {Key: "Fn+F2 / Fn+F3", Physical: "keyboard backlight", Runs: "the firmware and asusd; zephyrus-kbd-notify shows the level", From: "firmware"}, +} + +var ( + triggerLine = regexp.MustCompile(`^(\S+)\s+([0-9]+)\s+(.+)$`) + i3Bind = regexp.MustCompile(`^bindsym\s+((?:--\S+\s+)*)(\S+)\s+(.+)$`) + i3Exec = regexp.MustCompile(`^exec(?:_always)?\s+(?:--no-startup-id\s+)?(.+)$`) +) + +var triggerWhen = map[string]string{"0": "released", "1": "pressed", "2": "held (repeat)"} + +// Keys lists the laptop's custom keys: every triggerhappy trigger, the module's i3 lines and the keys +// the firmware handles itself. +func (m *Machine) Keys() KeysReport { + r := KeysReport{Keys: []Key{}, Warnings: []string{}, NotRead: []string{}} + seen := map[string][]string{} + files := m.glob(TriggerDir + "/*.conf") + sort.Strings(files) + if len(files) == 0 { + r.Warnings = append(r.Warnings, "no trigger file in "+TriggerDir+": the vendor keys do nothing") + } + for _, f := range files { + for _, line := range strings.Split(m.read(f), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + g := triggerLine.FindStringSubmatch(line) + if g == nil { + continue + } + r.Keys = append(r.Keys, Key{Key: g[1], Physical: physicalKeys[g[1]], When: triggerWhen[g[2]], Runs: g[3], From: f}) + id := g[1] + " " + g[2] + seen[id] = append(seen[id], f) + } + } + for id, fs := range seen { + if len(fs) > 1 { + r.Warnings = append(r.Warnings, fmt.Sprintf("%s is bound in %d places (%s): it fires every one", id, len(fs), strings.Join(fs, ", "))) + } + } + home := os.Getenv("MESH_OPERATOR_HOME") + if home == "" { + r.NotRead = append(r.NotRead, "the module's i3 lines (MESH_OPERATOR_HOME is not set)") + } + for _, rel := range I3Fragments { + if home == "" { + break + } + p := filepath.Join(home, rel) + text := m.read(p) + if text == "" { + r.Warnings = append(r.Warnings, "~/"+rel+" is missing or empty") + continue + } + for _, line := range strings.Split(text, "\n") { + line = strings.TrimSpace(line) + if g := i3Bind.FindStringSubmatch(line); g != nil { + when := "pressed" + if strings.Contains(g[1], "--release") { + when = "released" + } + r.Keys = append(r.Keys, Key{Key: g[2], Physical: physicalKeys[g[2]], When: when, Runs: strings.TrimPrefix(strings.TrimPrefix(g[3], "exec "), "--no-startup-id "), From: "~/" + rel}) + } else if g := i3Exec.FindStringSubmatch(line); g != nil { + r.Keys = append(r.Keys, Key{Key: "(session start)", When: "at login", Runs: g[1], From: "~/" + rel}) + } + } + } + r.Keys = append(r.Keys, firmwareKeys...) + sort.Strings(r.Warnings) + return r +} + +// asUserReferences finds the predecessor's as-user wrapper still named in a trigger, a udev rule or +// the module's i3 lines. +func (m *Machine) asUserReferences() []string { + var at []string + for _, pattern := range []string{TriggerDir + "/*.conf", "/etc/udev/rules.d/*.rules", "/etc/systemd/system-sleep/*"} { + for _, f := range m.glob(pattern) { + if strings.Contains(m.read(f), "as-user") { + at = append(at, f) + } + } + } + sort.Strings(at) + return at +} + +// triggerhappyUID is the real user id the running thd has, or -1 when none runs. +func (m *Machine) triggerhappyUID() int { + for _, dir := range m.glob("/proc/[0-9]*") { + if strings.TrimSpace(m.read(dir+"/comm")) != "thd" { + continue + } + for _, line := range strings.Split(m.read(dir+"/status"), "\n") { + if f := strings.Fields(line); len(f) >= 2 && f[0] == "Uid:" { + var uid int + if _, err := fmt.Sscanf(f[1], "%d", &uid); err == nil { + return uid + } + } + } + } + return -1 +} + +// operatorUID is the account the module's code runs as: the runtime runs it as the operator's. +var operatorUID = os.Getuid diff --git a/modules/asus-zephyrus-g14/cmd/zephyrus/keys_test.go b/modules/asus-zephyrus-g14/cmd/zephyrus/keys_test.go new file mode 100644 index 0000000..d073365 --- /dev/null +++ b/modules/asus-zephyrus-g14/cmd/zephyrus/keys_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func put(t *testing.T, root, p, content string) { + t.Helper() + full := filepath.Join(root, p) + if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +// The module's shipped triggers and i3 lines, read back as keys: what each runs and where. +func TestKeysListsTriggersI3LinesAndFirmwareKeys(t *testing.T) { + f := newFake(t) + m := readManifest(t) + content := map[string]string{} + for _, r := range m.Resources { + if c, ok := r["content"].(string); ok { + content[r["id"].(string)] = c + } + } + put(t, f.root, TriggerDir+"/asus-g14.conf", content["vendor-keys"]) + home := "/home/operator" + t.Setenv("MESH_OPERATOR_HOME", home) + put(t, f.root, home+"/"+I3Fragments[0], content["i3-vendor-keys"]) + put(t, f.root, home+"/"+I3Fragments[1], content["i3-model"]) + + r := f.machine().Keys() + got := map[string]Key{} + for _, k := range r.Keys { + got[k.Key+"|"+k.When] = k + } + if k := got["KEY_F21|pressed"]; !strings.Contains(k.Runs, "zephyrus-touchpad reset") || k.Physical == "" { + t.Errorf("touchpad key: %+v", k) + } + if k := got["KEY_PROG1|pressed"]; !strings.Contains(k.Runs, "zephyrus-media play-pause") { + t.Errorf("media key: %+v", k) + } + if k := got["$mod+Shift+s|released"]; !strings.Contains(k.Runs, "screenshot") || !strings.Contains(k.Physical, "Fn+F6") { + t.Errorf("screenshot key: %+v", k) + } + if k := got["$mod+p|pressed"]; !strings.Contains(k.Runs, "zephyrus-display order") { + t.Errorf("display key: %+v", k) + } + if k := got["(session start)|at login"]; k.Runs == "" { + t.Errorf("no session-start line read") + } + if k := got["Fn+F2 / Fn+F3|"]; k.From != "firmware" { + t.Errorf("firmware keys missing: %+v", r.Keys) + } + if len(r.Warnings) != 0 || len(r.NotRead) != 0 { + t.Errorf("warnings %v, not read %v", r.Warnings, r.NotRead) + } + if _, err := json.Marshal(r); err != nil { + t.Fatal(err) + } +} + +// A key bound in two trigger files fires twice, which is said. +func TestKeysWarnsAboutAKeyInTwoTriggerFiles(t *testing.T) { + f := newFake(t) + put(t, f.root, TriggerDir+"/asus-g14.conf", "KEY_F21\t1\t/x reset\n") + put(t, f.root, TriggerDir+"/old.conf", "KEY_F21\t1\t/y reset\n") + t.Setenv("MESH_OPERATOR_HOME", "") + r := f.machine().Keys() + if len(r.Warnings) != 1 || !strings.Contains(r.Warnings[0], "KEY_F21") || len(r.NotRead) != 1 { + t.Errorf("warnings %v, not read %v", r.Warnings, r.NotRead) + } +} + +// The check names what still calls as-user, and whether triggerhappy runs as the account. +func TestCheckFindsAsUserAndTriggerhappysAccount(t *testing.T) { + f := newFake(t) + put(t, f.root, "/etc/udev/rules.d/91-monitor-hotplug.rules", `RUN+="/x/scripts/as-user setsid wizard"`) + put(t, f.root, "/proc/4242/comm", "thd\n") + put(t, f.root, "/proc/4242/status", "Name:\tthd\nUid:\t0\t0\t0\t0\n") + was := operatorUID + operatorUID = func() int { return 1000 } + defer func() { operatorUID = was }() + t.Setenv("MESH_OPERATOR_HOME", "") + r := f.machine().Check(context.Background(), nil) + by := map[string]Check{} + for _, c := range r.Checks { + by[c.Name] = c + } + if c := by["no as-user"]; c.OK || !strings.Contains(c.Detail, "91-monitor-hotplug.rules") { + t.Errorf("as-user: %+v", c) + } + if c := by["triggerhappy as the account"]; c.OK || !strings.Contains(c.Detail, "uid 0") { + t.Errorf("triggerhappy: %+v", c) + } + if c := by["touchpad after resume"]; c.OK { + t.Errorf("resume: %+v", c) + } +} diff --git a/modules/asus-zephyrus-g14/cmd/zephyrus/tools.go b/modules/asus-zephyrus-g14/cmd/zephyrus/tools.go index 80c65c2..4d9b499 100644 --- a/modules/asus-zephyrus-g14/cmd/zephyrus/tools.go +++ b/modules/asus-zephyrus-g14/cmd/zephyrus/tools.go @@ -111,6 +111,13 @@ func Tools(m *Machine, sw *Switcher) []stdio.Tool { }, Run: func(args map[string]any) (any, error) { return FanCurvesTool(ctx(), m, args) }, }, + { + Name: "zephyrus_keys", + Description: "Every custom key on the laptop: each triggerhappy trigger (the vendor keys that reach no X client), the module's own i3 lines " + + "(keys the firmware sends as ordinary presses, and what starts with the session), and the keys the firmware handles itself — " + + "what each runs and the file it is defined in. Warns when one key is bound in two trigger files, which fires it twice.", + Run: func(map[string]any) (any, error) { return m.Keys(), nil }, + }, { Name: "zephyrus_check", Description: "Check what this module expects of the machine: the model, the vendor packages and daemons, the NVIDIA options in force, " + diff --git a/modules/asus-zephyrus-g14/files/bin/zephyrus-display b/modules/asus-zephyrus-g14/files/bin/zephyrus-display new file mode 100755 index 0000000..0b7d21b --- /dev/null +++ b/modules/asus-zephyrus-g14/files/bin/zephyrus-display @@ -0,0 +1,41 @@ +#!/bin/bash +# zephyrus-display primary | order — the laptop's internal panel among the outputs. +# Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# primary makes the internal panel X's primary output, where the bars' tray goes. +# order the display key (Fn+F9, which the firmware sends as Super+P): odd workspaces to the +# internal panel, even ones to the first external output, or all to the panel when it +# is alone. The predecessor's orden-workspaces. +# +# The panel is found, not named: the connected output whose name starts with eDP. The predecessor +# wrote eDP-1, which is what this model calls it today and not a promise. +set -u +here=$(dirname "$0") +panel=$(xrandr --query 2>/dev/null | awk '$2 == "connected" && $1 ~ /^eDP/ { print $1; exit }') +[ -n "$panel" ] || { echo "zephyrus-display: no internal panel connected" >&2; exit 1; } +case "${1:-}" in +primary) + exec xrandr --output "$panel" --primary + ;; +order) + external=$(xrandr --listmonitors 2>/dev/null | awk -v p="$panel" 'NR > 1 && $NF != p { print $NF; exit }') + [ -n "$external" ] || external=$panel + # i3's workspace list, one object per workspace; num and output read from each. No jq: the + # fields are flat strings and numbers, and rect, the only nested value, holds neither name. + i3-msg -t get_workspaces 2>/dev/null | sed 's/},{"id"/}\n{"id"/g' | + while read -r ws; do + num=$(printf '%s' "$ws" | grep -o '"num":-\?[0-9]*' | cut -d: -f2) + out=$(printf '%s' "$ws" | grep -o '"output":"[^"]*"' | cut -d'"' -f4) + [ -n "$num" ] && [ "$num" -ge 0 ] || continue + if [ $((num % 2)) -eq 0 ]; then dest=$external; else dest=$panel; fi + [ "$out" = "$dest" ] && continue + i3-msg "workspace number $num; move workspace to output $dest" >/dev/null + done + if [ "$external" = "$panel" ]; then + "$here/zephyrus-notify" 7780 "Workspaces: all on the panel" + else + "$here/zephyrus-notify" 7780 "Workspaces: odd on the panel, even on $external" + fi + ;; +*) echo "usage: zephyrus-display primary | order" >&2; exit 2 ;; +esac diff --git a/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify b/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify new file mode 100755 index 0000000..aee878b --- /dev/null +++ b/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify @@ -0,0 +1,20 @@ +#!/bin/bash +# zephyrus-kbd-notify — shows the keyboard backlight's level when it changes. The level itself is set +# by the firmware and asusd (Fn+F2/F3), which tell UPower; this only listens and notifies. Started once +# per session from the module's i3 fragment. Shipped by the mesh's asus-zephyrus-g14 module. +# +# One instance per session: i3 runs its `exec` lines again on an in-place restart, and the +# predecessor's listener ran twice after one, showing every change twice. +set -u +here=$(dirname "$0") +lock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/zephyrus-kbd-notify.lock" +exec 9>"$lock" +flock -n 9 || exit 0 +levels=(Off Low Med High) +dbus-monitor --system "type='signal',interface='org.freedesktop.UPower.KbdBacklight',member='BrightnessChanged'" 2>/dev/null | + while read -r line; do + if [[ $line =~ int32\ ([0-9]+) ]]; then + v=${BASH_REMATCH[1]} + "$here/zephyrus-notify" 5556 "Keyboard: ${levels[$v]:-$v}" + fi + done diff --git a/modules/asus-zephyrus-g14/files/bin/zephyrus-media b/modules/asus-zephyrus-g14/files/bin/zephyrus-media new file mode 100755 index 0000000..8b77d79 --- /dev/null +++ b/modules/asus-zephyrus-g14/files/bin/zephyrus-media @@ -0,0 +1,27 @@ +#!/bin/bash +# zephyrus-media play-pause | next | previous — the media keys, through MPRIS (playerctl), with a short +# notification of what happened. Shipped by the mesh's asus-zephyrus-g14 module; edit the catalogue. +# +# From the predecessor's media-control, kept: the lock against a double fire (the vendor keys can +# repeat), and the notification. Dropped: its fallback to a media server's local API, which needed a +# token from a file of secrets (novox/hq research 027 question 2). A player that speaks MPRIS is +# reached; one that does not says so. +set -u +here=$(dirname "$0") +action=${1:-play-pause} +case "$action" in play-pause | next | previous) ;; *) echo "usage: zephyrus-media play-pause | next | previous" >&2; exit 2 ;; esac +lock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/zephyrus-media.lock" +exec 9>"$lock" +flock -n 9 || exit 0 +if ! playerctl status >/dev/null 2>&1; then + "$here/zephyrus-notify" 7777 "Media: no player" + exit 0 +fi +playerctl "$action" 2>/dev/null +if [ "$action" = play-pause ]; then + sleep 0.2 + [ "$(playerctl status 2>/dev/null)" = Playing ] && label=Playing || label=Paused +else + label=${action^} +fi +"$here/zephyrus-notify" 7777 "Media: $label" diff --git a/modules/asus-zephyrus-g14/files/bin/zephyrus-session b/modules/asus-zephyrus-g14/files/bin/zephyrus-session index b74de75..0f21f91 100755 --- a/modules/asus-zephyrus-g14/files/bin/zephyrus-session +++ b/modules/asus-zephyrus-g14/files/bin/zephyrus-session @@ -13,9 +13,24 @@ export XDG_RUNTIME_DIR="/run/user/${uid}" export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" home=$(getent passwd "$uid" | cut -d: -f6) [ -n "$home" ] && export HOME="$home" +# The session's own window manager says best which display and authority the session uses: read +# them from its environment, as the desktop modules' session finder does. Then logind, then any +# process of this account that has a display. +from_environ() { + env=$(tr '\0' '\n' <"/proc/$1/environ" 2>/dev/null) || return 1 + d=$(printf '%s\n' "$env" | sed -n 's/^DISPLAY=//p' | head -n1) + [ -n "$d" ] || return 1 + export DISPLAY="$d" + a=$(printf '%s\n' "$env" | sed -n 's/^XAUTHORITY=//p' | head -n1) + [ -n "$a" ] && export XAUTHORITY="$a" + return 0 +} +if [ -z "${DISPLAY:-}" ]; then + for pid in $(pgrep -xu "$uid" i3 2>/dev/null); do + from_environ "$pid" && break + done +fi if [ -z "${DISPLAY:-}" ]; then - # The login manager may not record the display with logind; any process of this account that - # has one says which it is. for s in $(loginctl list-sessions --no-legend 2>/dev/null | awk -v u="$uid" '$2 == u { print $1 }'); do d=$(loginctl show-session "$s" -p Display --value 2>/dev/null) if [ -n "$d" ]; then export DISPLAY="$d"; break; fi @@ -23,14 +38,7 @@ if [ -z "${DISPLAY:-}" ]; then fi if [ -z "${DISPLAY:-}" ]; then for pid in $(pgrep -u "$uid" 2>/dev/null); do - env=$(tr '\0' '\n' <"/proc/$pid/environ" 2>/dev/null) || continue - d=$(printf '%s\n' "$env" | sed -n 's/^DISPLAY=//p' | head -n1) - if [ -n "$d" ]; then - export DISPLAY="$d" - a=$(printf '%s\n' "$env" | sed -n 's/^XAUTHORITY=//p' | head -n1) - [ -n "$a" ] && export XAUTHORITY="$a" - break - fi + from_environ "$pid" && break done fi : "${XAUTHORITY:=${HOME}/.Xauthority}" diff --git a/modules/asus-zephyrus-g14/module.json b/modules/asus-zephyrus-g14/module.json index 863c95b..2751844 100644 --- a/modules/asus-zephyrus-g14/module.json +++ b/modules/asus-zephyrus-g14/module.json @@ -5,6 +5,9 @@ "package-manager", "service-manager" ], + "requires": [ + "x11-display" + ], "emits": [ "profile.switched" ], @@ -18,6 +21,7 @@ "zephyrus_power_draw", "zephyrus_profile_policy", "zephyrus_fan_curves", + "zephyrus_keys", "zephyrus_check" ], "resources": [ @@ -31,11 +35,6 @@ "type": "package", "package": "playerctl" }, - { - "id": "xinput", - "type": "package", - "package": "xorg-xinput" - }, { "id": "asusd", "type": "service", @@ -175,7 +174,7 @@ "type": "file", "path": "/etc/triggerhappy/triggers.d/asus-g14.conf", "mode": "0644", - "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The ROG Zephyrus G14's vendor keys, which reach no X client. Run as the operator's account (see the\n# module's drop-in on triggerhappy.service).\nKEY_PROG1\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session playerctl play-pause\nKEY_PROG3\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session playerctl previous\nKEY_PROG4\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session playerctl next\nKEY_BRIGHTNESSDOWN\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSDOWN\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSUP\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_BRIGHTNESSUP\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_F21\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n" + "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The ROG Zephyrus G14's vendor keys, which reach no X client. Run as the operator's account (see the\n# module's drop-in on triggerhappy.service).\nKEY_PROG1\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media play-pause\nKEY_PROG3\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media previous\nKEY_PROG4\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-session /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-media next\nKEY_BRIGHTNESSDOWN\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSDOWN\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight -\nKEY_BRIGHTNESSUP\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_BRIGHTNESSUP\t2\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-backlight +\nKEY_F21\t1\t/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n" }, { "id": "triggerhappy", @@ -186,7 +185,11 @@ "restart-on": [ "triggerhappy-as-account", "vendor-keys", - "scripts" + "scripts", + "touchpad-on-resume", + "touchpad-after-suspend", + "touchpad-after-hibernate", + "touchpad-after-suspend-then-hibernate" ] }, { @@ -229,6 +232,50 @@ "path": "/etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf", "mode": "0644", "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings, applied by X every time the device appears — at login and after every\n# resume, when the device is initialised again. This replaces the predecessor's sleep hook, which ran\n# xinput after a resume as a named person on a guessed display.\nSection \"InputClass\"\n Identifier \"asus-zephyrus-g14 touchpad\"\n MatchIsTouchpad \"on\"\n Option \"Tapping\" \"on\"\n Option \"NaturalScrolling\" \"true\"\n Option \"AccelSpeed\" \"0.15\"\nEndSection\n" + }, + { + "id": "touchpad-on-resume", + "type": "file", + "path": "/etc/systemd/system/asus-zephyrus-g14-touchpad-resume.service", + "mode": "0644", + "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# After a resume, the touchpad's settings once more in the operator's session. X applies the module's\n# input class whenever the device appears, which covers a resume that initialises the device again;\n# this covers one that does not, as the predecessor's sleep hook did. Started by the sleep services\n# themselves (their asus-zephyrus-g14-touchpad.conf drop-ins), never enabled, and ordered after them,\n# so it runs once the machine is awake.\n[Unit]\nDescription=Touchpad settings after resume (asus-zephyrus-g14)\nAfter=systemd-suspend.service systemd-hibernate.service systemd-suspend-then-hibernate.service\n\n[Service]\nType=oneshot\nUser=${machine:account}\nExecStartPre=/bin/sleep 2\nExecStart=/usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n" + }, + { + "id": "touchpad-after-suspend", + "type": "file", + "path": "/etc/systemd/system/systemd-suspend.service.d/asus-zephyrus-g14-touchpad.conf", + "mode": "0644", + "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings again once the machine is awake (asus-zephyrus-g14-touchpad-resume.service).\n[Unit]\nWants=asus-zephyrus-g14-touchpad-resume.service\n" + }, + { + "id": "touchpad-after-hibernate", + "type": "file", + "path": "/etc/systemd/system/systemd-hibernate.service.d/asus-zephyrus-g14-touchpad.conf", + "mode": "0644", + "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings again once the machine is awake (asus-zephyrus-g14-touchpad-resume.service).\n[Unit]\nWants=asus-zephyrus-g14-touchpad-resume.service\n" + }, + { + "id": "touchpad-after-suspend-then-hibernate", + "type": "file", + "path": "/etc/systemd/system/systemd-suspend-then-hibernate.service.d/asus-zephyrus-g14-touchpad.conf", + "mode": "0644", + "content": "# Managed by the mesh (module asus-zephyrus-g14). Replaced on every push; edit the catalogue instead.\n#\n# The touchpad's settings again once the machine is awake (asus-zephyrus-g14-touchpad-resume.service).\n[Unit]\nWants=asus-zephyrus-g14-touchpad-resume.service\n" + }, + { + "id": "i3-vendor-keys", + "type": "file", + "path": "${machine:account-home}/.config/i3/config.d/10-asus.conf", + "owner": "${machine:account}", + "mode": "0644", + "content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are\n# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for\n# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's.\n\n# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the\n# screenshot grabs the pointer to select a region, which fails while the key is still held.\nbindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh\n\n# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the\n# external output.\nbindsym $mod+p exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display order\n\n# The keyboard backlight's level, shown when it changes.\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-kbd-notify\n" + }, + { + "id": "i3-model", + "type": "file", + "path": "${machine:account-home}/.config/i3/config.d/20-g14.conf", + "owner": "${machine:account}", + "mode": "0644", + "content": "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The model's panel and touchpad.\n\n# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where\n# is the display server's (autorandr, run at every session start).\nexec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-display primary\n\n# The touchpad's settings again, by hand. X applies them itself whenever the device appears.\nbindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset\n" } ], "build": {