diff --git a/system_files/etc/gamescope-session-plus/sessions.d/steam b/system_files/etc/gamescope-session-plus/sessions.d/steam index 258bf80..8a6e958 100644 --- a/system_files/etc/gamescope-session-plus/sessions.d/steam +++ b/system_files/etc/gamescope-session-plus/sessions.d/steam @@ -14,6 +14,12 @@ if [[ -n "${ARMADA_PANEL_REFRESH_RATES:-}" ]]; then unset STEAM_DISPLAY_REFRESH_LIMITS fi fi +# Gamescope only lights up the primary panel; silence the secondary +# touchscreen so fingers on the dark glass don't move Steam's cursor. +# The desktop session re-enables it (see desktop-bootstrap). +if [[ -n "${ARMADA_SECONDARY_TOUCHSCREEN:-}" ]]; then + /usr/libexec/armada/touchscreen-inhibit "$ARMADA_SECONDARY_TOUCHSCREEN" 1 2>/dev/null || true +fi USE_ROTATION_SHADER="${ARMADA_GAMESCOPE_USE_ROTATION_SHADER:-0}" if [[ -n "${ARMADA_GAMESCOPE_FAKE_OUTPUT_MM:-}" ]]; then export GAMESCOPE_FAKE_OUTPUT_MM="$ARMADA_GAMESCOPE_FAKE_OUTPUT_MM" diff --git a/system_files/etc/xdg/powerdevilrc b/system_files/etc/xdg/powerdevilrc new file mode 100644 index 0000000..4e2fd7b --- /dev/null +++ b/system_files/etc/xdg/powerdevilrc @@ -0,0 +1,10 @@ +# Armada: no automatic idle suspend. PowerDevil's idle detection does not +# count gamepad input as activity, so with the persistent desktop session +# (nested gaming) it would suspend mid-game. Suspend stays available via the +# power button (powerbuttond -> fake-suspend). Users can still override this +# per-user in System Settings. +[AC][SuspendAndShutdown] +AutoSuspendAction=0 + +[Battery][SuspendAndShutdown] +AutoSuspendAction=0 diff --git a/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write b/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write index 412df77..a039f53 100755 --- a/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write +++ b/system_files/usr/bin/steamos-polkit-helpers/steamos-priv-write @@ -72,5 +72,30 @@ if [[ "$WRITE_PATH" == /sys/class/hwmon/hwmon*/power*_cap ]]; then exit 1 fi +if [[ "$WRITE_PATH" == /sys/class/backlight/*/brightness ]]; then + [[ "$WRITE_VALUE" =~ ^[0-9]+$ ]] || exit 1 + # Steam adjusts the first backlight it finds; on dual-screen devices + # that can be the secondary panel. Steer writes to the primary + # backlight, rescaling between the two devices' brightness ranges. + eval "$(/usr/libexec/armada/device-env)" + src_dir="$(dirname "$WRITE_PATH")" + tgt_dir="$src_dir" + if [[ -n "${ARMADA_PRIMARY_BACKLIGHT:-}" ]]; then + tgt_dir="/sys/class/backlight/${ARMADA_PRIMARY_BACKLIGHT}" + fi + [[ -e "$tgt_dir/brightness" ]] || exit 1 + val="$WRITE_VALUE" + if [[ "$tgt_dir" != "$src_dir" && -e "$src_dir/max_brightness" ]]; then + src_max="$(cat "$src_dir/max_brightness")" + tgt_max="$(cat "$tgt_dir/max_brightness")" + if [[ "$src_max" -gt 0 && "$src_max" -ne "$tgt_max" ]]; then + val=$(( val * tgt_max / src_max )) + fi + fi + echo "$val" > "$tgt_dir/brightness" + log "backlight: $WRITE_VALUE -> $tgt_dir/brightness ($val)" + exit 0 +fi + echo "refusing unsupported write: $WRITE_VALUE -> $WRITE_PATH" >&2 exit 1 diff --git a/system_files/usr/lib/armada/devices/ayn-thor.conf b/system_files/usr/lib/armada/devices/ayn-thor.conf index b063aab..8ac6497 100644 --- a/system_files/usr/lib/armada/devices/ayn-thor.conf +++ b/system_files/usr/lib/armada/devices/ayn-thor.conf @@ -2,7 +2,15 @@ ARMADA_DEVICE_ID=ayn-thor ARMADA_DEVICE_NAME='AYN Thor' ARMADA_SOC_CLASS=SM8550 -ARMADA_PRIMARY_CONNECTOR=DSI-1 +# Dual screen: top panel is DSI-2 (mdss_dsi1), bottom panel is DSI-1 +# (mdss_dsi0). Gamescope drives only the top panel; the desktop session +# uses both (see desktop-bootstrap). +ARMADA_PRIMARY_CONNECTOR=DSI-2 +ARMADA_PRIMARY_BACKLIGHT=ae96000.dsi.0 +ARMADA_PRIMARY_TOUCHSCREEN=top_touchscreen +ARMADA_SECONDARY_CONNECTOR=DSI-1 +ARMADA_SECONDARY_TOUCHSCREEN=bottom_touchscreen +ARMADA_GAMING_SESSION=nested ARMADA_PANEL_ORIENTATION=right ARMADA_PANEL_NATIVE_WIDTH=1080 ARMADA_PANEL_NATIVE_HEIGHT=1920 diff --git a/system_files/usr/lib/armada/devices/defaults.conf b/system_files/usr/lib/armada/devices/defaults.conf index 1b162c5..d4fab41 100644 --- a/system_files/usr/lib/armada/devices/defaults.conf +++ b/system_files/usr/lib/armada/devices/defaults.conf @@ -3,6 +3,19 @@ ARMADA_DEVICE_NAME=unknown ARMADA_SOC_CLASS= ARMADA_PRIMARY_CONNECTOR= +# Backlight Steam brightness writes are steered to. Only needed on devices +# with more than one backlight (Steam grabs the first one it finds). +ARMADA_PRIMARY_BACKLIGHT= +# Set on dual-screen devices; the desktop session extends onto this output. +ARMADA_SECONDARY_CONNECTOR= +# Input device names used to pin each touchscreen to its own output in the +# desktop session (only needed when a secondary connector is set). +ARMADA_PRIMARY_TOUCHSCREEN= +ARMADA_SECONDARY_TOUCHSCREEN= +# embedded (default): gamescope owns the GPU in gaming mode. +# nested: gaming mode runs as a nested gamescope inside the desktop session +# (dual-screen devices), making mode switches near-instant. +ARMADA_GAMING_SESSION=embedded ARMADA_PANEL_TYPE=internal ARMADA_PANEL_ORIENTATION=normal ARMADA_PANEL_NATIVE_WIDTH= diff --git a/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules b/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules index 306a54e..8187b68 100644 --- a/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules +++ b/system_files/usr/lib/udev/rules.d/60-armada-perf-acls.rules @@ -6,3 +6,6 @@ SUBSYSTEM=="devfreq", KERNEL=="*.gpu", \ SUBSYSTEM=="backlight", \ RUN+="/bin/sh -c 'chgrp wheel /sys/class/backlight/%k/brightness 2>/dev/null; chmod g+w /sys/class/backlight/%k/brightness 2>/dev/null'" + +SUBSYSTEM=="input", KERNEL=="input[0-9]*", \ + RUN+="/bin/sh -c 'chgrp wheel /sys/class/input/%k/inhibited 2>/dev/null; chmod g+w /sys/class/input/%k/inhibited 2>/dev/null'" diff --git a/system_files/usr/libexec/armada/desktop-bootstrap b/system_files/usr/libexec/armada/desktop-bootstrap index 1bd9420..b6dda54 100755 --- a/system_files/usr/libexec/armada/desktop-bootstrap +++ b/system_files/usr/libexec/armada/desktop-bootstrap @@ -20,17 +20,31 @@ else systemctl --user import-environment "${activation_env[@]}" 2>/dev/null || true fi +if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then + eval "$(/usr/libexec/armada/device-env)" +fi + +# The gaming session inhibits the secondary touchscreen (the desktop is the +# only session that lights up the secondary panel); re-enable it every start. +if [[ -n "${ARMADA_SECONDARY_TOUCHSCREEN:-}" ]]; then + /usr/libexec/armada/touchscreen-inhibit "$ARMADA_SECONDARY_TOUCHSCREEN" 0 2>/dev/null || true +fi + +# Nested-gaming devices boot into this session with gaming on the primary +# screen; a fresh session start is either boot or an explicit return to +# gamemode (Switch to Desktop keeps the session and never re-runs this). +if [[ ${ARMADA_GAMING_SESSION:-} == nested ]] && ! pgrep -x gamescope-wl >/dev/null; then + systemd-run --user --collect /usr/libexec/armada/nested-gaming 2>/dev/null || true +fi + config_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/armada" rotation_done="${config_dir}/desktop-rotation.done" scale_done="${config_dir}/desktop-scale.done" +dual_done="${config_dir}/desktop-dualscreen.done" -[[ ! -e "${rotation_done}" || ! -e "${scale_done}" ]] || exit 0 +[[ ! -e "${rotation_done}" || ! -e "${scale_done}" || ! -e "${dual_done}" ]] || exit 0 mkdir -p "${config_dir}" -if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then - eval "$(/usr/libexec/armada/device-env)" -fi - display_connector="${ARMADA_PRIMARY_CONNECTOR:-}" panel_orientation="${ARMADA_PANEL_ORIENTATION:-normal}" @@ -57,3 +71,11 @@ fi if [[ ! -e "${scale_done}" ]] && kscreen-doctor "output.${display_connector}.scale.1.5" >/dev/null 2>&1; then touch "${scale_done}" fi + +if [[ ! -e "${dual_done}" ]]; then + if [[ -z "${ARMADA_SECONDARY_CONNECTOR:-}" ]]; then + touch "${dual_done}" + elif /usr/libexec/armada/setup-dual-screen; then + touch "${dual_done}" + fi +fi diff --git a/system_files/usr/libexec/armada/device-env b/system_files/usr/libexec/armada/device-env index faf005f..f505ceb 100755 --- a/system_files/usr/libexec/armada/device-env +++ b/system_files/usr/libexec/armada/device-env @@ -36,6 +36,11 @@ vars=( ARMADA_DEVICE_NAME ARMADA_SOC_CLASS ARMADA_PRIMARY_CONNECTOR + ARMADA_PRIMARY_BACKLIGHT + ARMADA_SECONDARY_CONNECTOR + ARMADA_PRIMARY_TOUCHSCREEN + ARMADA_SECONDARY_TOUCHSCREEN + ARMADA_GAMING_SESSION ARMADA_PANEL_TYPE ARMADA_PANEL_ORIENTATION ARMADA_PANEL_NATIVE_WIDTH diff --git a/system_files/usr/libexec/armada/nested-gaming b/system_files/usr/libexec/armada/nested-gaming new file mode 100755 index 0000000..ce6dec7 --- /dev/null +++ b/system_files/usr/libexec/armada/nested-gaming @@ -0,0 +1,40 @@ +#!/usr/bin/bash +# Gaming mode on dual-screen devices (ARMADA_GAMING_SESSION=nested): run +# gamescope+Steam as a nested fullscreen window on the primary screen of the +# already-running desktop session. steamos-manager starts and stops this; +# see docs/superpowers/specs/2026-07-07-thor-second-screen-phase2-design.md. +set -euo pipefail + +pgrep -x gamescope-wl >/dev/null && exit 0 + +eval "$(/usr/libexec/armada/device-env)" + +# The desktop shows rotated panels in landscape; gamescope renders landscape. +width="${ARMADA_PANEL_NATIVE_WIDTH:-1080}" +height="${ARMADA_PANEL_NATIVE_HEIGHT:-1920}" +case "${ARMADA_PANEL_ORIENTATION:-normal}" in + left|right) gs_w="$height" gs_h="$width" ;; + *) gs_w="$width" gs_h="$height" ;; +esac + +# Expose the refresh range so Steam offers per-game refresh switching; +# nested-refresh-bridge applies requests to the host compositor. +if [[ -n "${ARMADA_PANEL_REFRESH_RATES:-}" ]]; then + IFS=',' read -r -a _rates <<< "$ARMADA_PANEL_REFRESH_RATES" + if (( ${#_rates[@]} > 1 )); then + export STEAM_DISPLAY_REFRESH_LIMITS="${_rates[0]},${_rates[-1]}" + fi + # Advertised to Steam via gamescope_control (armada gamescope patch); + # the Wayland backend cannot enumerate the host output's modes itself. + export GAMESCOPE_NESTED_REFRESH_RATES="$ARMADA_PANEL_REFRESH_RATES" + /usr/libexec/armada/nested-refresh-bridge & +fi + +# --prefer-output pins the fullscreen window to the primary screen (armada +# gamescope patch: the Wayland backend passes it to the fullscreen request; +# otherwise the host compositor picks whichever output was last active). +prefer_output=() +[[ -n "${ARMADA_PRIMARY_CONNECTOR:-}" ]] && prefer_output=(--prefer-output "$ARMADA_PRIMARY_CONNECTOR") + +exec gamescope -e --fullscreen -W "$gs_w" -H "$gs_h" --steam "${prefer_output[@]}" -- \ + /usr/libexec/armada/launch-steam -gamepadui -steamos3 -steampal -steamdeck -noverifyfiles diff --git a/system_files/usr/libexec/armada/nested-refresh-bridge b/system_files/usr/libexec/armada/nested-refresh-bridge new file mode 100755 index 0000000..cfc7daa --- /dev/null +++ b/system_files/usr/libexec/armada/nested-refresh-bridge @@ -0,0 +1,71 @@ +#!/usr/bin/bash +# Bridge Steam's refresh-rate requests to KWin for the nested gaming session. +# +# In the embedded session gamescope owns the display and applies +# GAMESCOPE_DYNAMIC_REFRESH (set by Steam on gamescope's Xwayland root) +# itself; nested gamescope cannot modeset the host output. Watch the atom on +# the nested Xwayland and apply the nearest supported panel rate through +# kscreen-doctor. Spawned by nested-gaming; exits with the session. +set -euo pipefail + +eval "$(/usr/libexec/armada/device-env)" + +[[ ${ARMADA_GAMING_SESSION:-} == nested ]] || exit 0 +[[ -n ${ARMADA_PRIMARY_CONNECTOR:-} && -n ${ARMADA_PANEL_REFRESH_RATES:-} ]] || exit 0 +[[ -n ${ARMADA_PANEL_NATIVE_WIDTH:-} && -n ${ARMADA_PANEL_NATIVE_HEIGHT:-} ]] || exit 0 + +IFS=',' read -r -a rates <<< "$ARMADA_PANEL_REFRESH_RATES" +max_rate="${rates[-1]}" +mode_res="${ARMADA_PANEL_NATIVE_WIDTH}x${ARMADA_PANEL_NATIVE_HEIGHT}" + +# The nested gamescope's Xwayland is where Steam sets the atom; find its +# display number from Steam's environment. +nested_display="" +for _ in $(seq 1 60); do + steam_pid=$(pgrep -x steam | head -1 || true) + if [[ -n "$steam_pid" ]]; then + nested_display=$(tr '\0' '\n' < "/proc/${steam_pid}/environ" 2>/dev/null \ + | sed -n 's/^DISPLAY=//p' | head -1) + [[ -n "$nested_display" ]] && break + fi + sleep 2 +done +[[ -n "$nested_display" ]] || exit 0 + +snap_rate() { + local want=$1 best="${rates[0]}" best_delta=-1 rate delta + for rate in "${rates[@]}"; do + delta=$(( want > rate ? want - rate : rate - want )) + # Ties go to the higher rate: rates are listed ascending. + if (( best_delta < 0 || delta <= best_delta )); then + best=$rate best_delta=$delta + fi + done + printf '%s' "$best" +} + +current="" +apply() { + local hz=$1 + [[ "$hz" =~ ^[0-9]+$ ]] || hz=0 + (( hz == 0 )) && hz=$max_rate + hz=$(snap_rate "$hz") + [[ "$hz" == "$current" ]] && return 0 + if kscreen-doctor "output.${ARMADA_PRIMARY_CONNECTOR}.mode.${mode_res}@${hz}" >/dev/null 2>&1; then + current=$hz + fi +} + +# Seed the atom so xprop -spy has a property to watch from the start. +xprop -display "$nested_display" -root -f GAMESCOPE_DYNAMIC_REFRESH 32c \ + -set GAMESCOPE_DYNAMIC_REFRESH 0 2>/dev/null || exit 0 + +# stdbuf: xprop block-buffers when piped, which would delay events forever. +stdbuf -oL xprop -display "$nested_display" -root -spy GAMESCOPE_DYNAMIC_REFRESH 2>/dev/null \ + | while read -r line; do + apply "${line##*= }" + done + +# Nested Xwayland is gone (gaming session ended): restore the default rate. +current="" +apply "$max_rate" diff --git a/system_files/usr/libexec/armada/session-control b/system_files/usr/libexec/armada/session-control index 3111c85..95f5fdb 100755 --- a/system_files/usr/libexec/armada/session-control +++ b/system_files/usr/libexec/armada/session-control @@ -2,10 +2,19 @@ # zz- sorts after armada.conf so its Session= wins (User=/Relogin= stay there). set -euo pipefail +# On nested-gaming devices the desktop session hosts gaming mode as a +# nested gamescope (launched by desktop-bootstrap), so "gamemode" boots +# into the plasma session too. +gamemode_session=gamescope-session-steam.desktop +if command -v /usr/libexec/armada/device-env >/dev/null 2>&1; then + eval "$(/usr/libexec/armada/device-env)" + [[ ${ARMADA_GAMING_SESSION:-} == nested ]] && gamemode_session=armada-plasma.desktop +fi + case "${1:-}" in switch-desktop) session=armada-plasma.desktop ;; - switch-gamemode) session=gamescope-session-steam.desktop ;; - default-gamemode) session=gamescope-session-steam.desktop; default_only=1 ;; + switch-gamemode) session=$gamemode_session ;; + default-gamemode) session=$gamemode_session; default_only=1 ;; *) echo "usage: $0 {switch-desktop|switch-gamemode|default-gamemode}" >&2; exit 1 ;; esac diff --git a/system_files/usr/libexec/armada/setup-dual-screen b/system_files/usr/libexec/armada/setup-dual-screen new file mode 100755 index 0000000..08afbb3 --- /dev/null +++ b/system_files/usr/libexec/armada/setup-dual-screen @@ -0,0 +1,124 @@ +#!/usr/bin/python3 +"""One-time desktop layout for dual-screen devices, run by desktop-bootstrap. + +Places the secondary output below the primary (centered), makes the primary +the priority-1 output, and pins each touchscreen to its own output so taps +land on the screen being touched. +""" +import glob +import json +import shlex +import subprocess +import sys + + +def device_env(): + out = subprocess.run( + ["/usr/libexec/armada/device-env"], + capture_output=True, text=True, check=True, + ).stdout + env = {} + for line in out.splitlines(): + key, _, value = line.partition("=") + env[key] = shlex.split(value)[0] if value else "" + return env + + +def logical_size(output): + mode = next( + (m for m in output.get("modes", []) + if str(m.get("id")) == str(output.get("currentModeId"))), + None, + ) + if mode is None: + return None + width = mode["size"]["width"] + height = mode["size"]["height"] + # KScreen rotation flags: 2 (left) and 8 (right) swap the axes. + if output.get("rotation") in (2, 8): + width, height = height, width + scale = output.get("scale") or 1 + return round(width / scale), round(height / scale) + + +def input_devices(): + """Map input device name -> (vendor, product, event node).""" + devices = {} + for sysdir in glob.glob("/sys/class/input/input*"): + try: + name = open(f"{sysdir}/name").read().strip() + vendor = int(open(f"{sysdir}/id/vendor").read(), 16) + product = int(open(f"{sysdir}/id/product").read(), 16) + event = next( + e.rsplit("/", 1)[-1] for e in glob.glob(f"{sysdir}/event*") + ) + except (OSError, StopIteration, ValueError): + continue + devices[name] = (vendor, product, event) + return devices + + +def pin_touchscreen(name, connector, devices): + if name not in devices: + return + vendor, product, event = devices[name] + # Persisted mapping, applied by KWin when the device is added. + subprocess.run( + ["kwriteconfig6", "--file", "kcminputrc", + "--group", "Libinput", "--group", str(vendor), + "--group", str(product), "--group", name, + "--key", "OutputName", connector], + check=False, + ) + # Live mapping for the current session. + subprocess.run( + ["gdbus", "call", "--session", "--dest", "org.kde.KWin", + "--object-path", f"/org/kde/KWin/InputDevice/{event}", + "--method", "org.freedesktop.DBus.Properties.Set", + "org.kde.KWin.InputDevice", "outputName", f"<'{connector}'>"], + check=False, capture_output=True, + ) + + +def main(): + env = device_env() + primary = env.get("ARMADA_PRIMARY_CONNECTOR") + secondary = env.get("ARMADA_SECONDARY_CONNECTOR") + if not primary or not secondary: + return 0 + + # Some kscreen-doctor versions append human-readable text after the + # JSON document, so parse only the first document. + doc = subprocess.run( + ["kscreen-doctor", "-j"], + capture_output=True, text=True, check=True, + ).stdout + outputs = json.JSONDecoder().raw_decode(doc)[0]["outputs"] + by_name = {o["name"]: o for o in outputs} + if primary not in by_name or secondary not in by_name: + print(f"setup-dual-screen: outputs not found: {primary}, {secondary}", + file=sys.stderr) + return 1 + + primary_size = logical_size(by_name[primary]) + secondary_size = logical_size(by_name[secondary]) + if primary_size is None or secondary_size is None: + return 1 + x = max(0, round((primary_size[0] - secondary_size[0]) / 2)) + subprocess.run( + ["kscreen-doctor", + f"output.{primary}.priority.1", + f"output.{secondary}.priority.2", + f"output.{primary}.position.0,0", + f"output.{secondary}.position.{x},{primary_size[1]}"], + check=True, + ) + + devices = input_devices() + pin_touchscreen(env.get("ARMADA_PRIMARY_TOUCHSCREEN"), primary, devices) + pin_touchscreen(env.get("ARMADA_SECONDARY_TOUCHSCREEN"), secondary, devices) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/system_files/usr/libexec/armada/steamos-manager b/system_files/usr/libexec/armada/steamos-manager index 2a54634..221d26f 100755 --- a/system_files/usr/libexec/armada/steamos-manager +++ b/system_files/usr/libexec/armada/steamos-manager @@ -246,7 +246,38 @@ class ArmadaSteamOSManager: return raise KeyError(prop) + @staticmethod + def _process_running(name): + return ( + subprocess.run( + ["pgrep", "-x", name], stdout=subprocess.DEVNULL + ).returncode + == 0 + ) + def switch_session(self, target): + # On dual-screen devices the desktop compositor stays up and hosts + # gaming mode as a nested gamescope on the primary screen; switching + # starts or stops that gamescope instead of swapping SDDM sessions. + # Only the session-bus instance can manage the user's session. + if ( + self.env.get("ARMADA_GAMING_SESSION") == "nested" + and os.geteuid() != 0 + and self._process_running("kwin_wayland") + ): + if target == "desktop": + if self._process_running("gamescope-wl"): + subprocess.run(["pkill", "-x", "gamescope-wl"], timeout=10) + return + if target == "gamemode": + subprocess.run( + ["systemd-run", "--user", "--collect", + "/usr/libexec/armada/nested-gaming"], + check=True, + timeout=10, + ) + return + commands = { "desktop": "switch-desktop", "gamemode": "switch-gamemode", diff --git a/system_files/usr/libexec/armada/touchscreen-inhibit b/system_files/usr/libexec/armada/touchscreen-inhibit new file mode 100755 index 0000000..7ebc8ab --- /dev/null +++ b/system_files/usr/libexec/armada/touchscreen-inhibit @@ -0,0 +1,12 @@ +#!/usr/bin/bash +# Inhibit (1) or re-enable (0) an input device by name. Used to silence the +# secondary touchscreen while gamescope owns the only visible screen. +set -euo pipefail + +name="${1:?usage: touchscreen-inhibit <1|0>}" +state="${2:?usage: touchscreen-inhibit <1|0>}" + +for dev in /sys/class/input/input*/; do + [[ "$(cat "${dev}name" 2>/dev/null)" == "${name}" ]] || continue + echo "${state}" > "${dev}inhibited" +done