mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
Take over the volume keys so they can't crash gamescope
gamescope sends volume up and down to Steam by moving keyboard focus to Steam for the key and back. When nothing had focus, it moves focus back to null and wlroots aborts, which ends the whole VR session. One press of the headset's volume button did that while working in the Frametop desktop. The input relay now handles volume keys from every device that has them, the headset's buttons included, and steps the default output with wpctl (5%, repeating while held). SteamVR, which passes keys on to gamescope, never sees a volume key: devices with a keymap (gpio-keys, USB and Bluetooth keyboards) get only their volume entries remapped to unused codes, so the headset's click button keeps working, and pmic_resin, which has only volume down, is grabbed. The keymaps go back when the relay stops, and --no-grab leaves the volume keys alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de2823c360
commit
705b434cf3
3 files changed
+170
-12
No files matched your search
@@ -100,6 +100,8 @@ SteamVR opens every input device only when it starts. When a Bluetooth mouse sle
|
|||||||
|
|
||||||
Keyboards aren't grabbed by default, because a grabbed keyboard's keys went into a virtual keyboard nothing typed from; the relay forwards them to ft-screens instead.
|
Keyboards aren't grabbed by default, because a grabbed keyboard's keys went into a virtual keyboard nothing typed from; the relay forwards them to ft-screens instead.
|
||||||
|
|
||||||
|
Volume keys must never reach gamescope. With the openvr backend, gamescope sends volume up and down to Steam by moving keyboard focus to Steam for the key and then back to the previously focused surface. When nothing had focus, the one it moves back to is null, and wlroots aborts on a null focus surface (`wlr_seat_keyboard_notify_enter: Assertion 'surface' failed`), which ends the whole VR session. Keyboard focus is often empty while you work in VR, so one press of the headset's volume button could take everything down. SteamVR reads the headset's buttons itself and passes keys on to gamescope, so the relay has to stop volume keys before SteamVR sees them. Grabbing `gpio-keys` would also take the headset's click button, so the relay remaps the volume entries in each device's keymap (`EVIOCSKEYCODE`) and handles the stand-in codes itself. That fix covers every device at once, including keyboards that aren't grabbed.
|
||||||
|
|
||||||
## The desktop session
|
## The desktop session
|
||||||
|
|
||||||
The session is modeled on SteamOS's `steamos-nested-desktop` and runs beside it. It has its own runtime directory, config (`~/.config/frametop`), and state, so it never disturbs the stock desktop's layout or panels. It runs on a private D-Bus from `dbus-run-session`, which has two consequences. KDE only launches apps in systemd scopes when systemd is on the session bus, so everything started in the desktop lands in its systemd unit, and stopping the unit would kill all of it; `session/keep-apps.sh` moves those programs out first. And tools that need the real user bus, like podman and `distrobox-host-exec`, have to be pointed at it explicitly.
|
The session is modeled on SteamOS's `steamos-nested-desktop` and runs beside it. It has its own runtime directory, config (`~/.config/frametop`), and state, so it never disturbs the stock desktop's layout or panels. It runs on a private D-Bus from `dbus-run-session`, which has two consequences. KDE only launches apps in systemd scopes when systemd is on the session bus, so everything started in the desktop lands in its systemd unit, and stopping the unit would kill all of it; `session/keep-apps.sh` moves those programs out first. And tools that need the real user bus, like podman and `distrobox-host-exec`, have to be pointed at it explicitly.
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ SteamVR opens input devices only when it starts. A Bluetooth mouse that sleeps a
|
|||||||
|
|
||||||
It runs as the user service `frametop-input-relay.service`, ordered before `steamvr.service`.
|
It runs as the user service `frametop-input-relay.service`, ordered before `steamvr.service`.
|
||||||
|
|
||||||
|
The relay also owns the volume keys, on every device that has them, the headset's buttons included. It changes the volume itself (`wpctl`, 5% a step, repeating while held), and SteamVR never sees a volume key: on devices with a keymap (the headset's `gpio-keys`, USB and Bluetooth keyboards) it remaps just the volume entries to unused codes (`KEY_MACRO29`, `KEY_MACRO30`), so the headset's click button and the other keys still reach SteamVR, and it grabs `pmic_resin`, which has only volume down. The keymaps go back when the relay stops. With `--no-grab` it leaves the volume keys alone.
|
||||||
|
|
||||||
```
|
```
|
||||||
desktops.sh relay install # enable it (starts with the next reboot or SteamVR start)
|
desktops.sh relay install # enable it (starts with the next reboot or SteamVR start)
|
||||||
desktops.sh relay status | log | uninstall
|
desktops.sh relay status | log | uninstall
|
||||||
|
|||||||
+166
-12
@@ -26,6 +26,14 @@ Keys also go to ft-screens (@ft_screens, the Frametop desktop's compositor), whi
|
|||||||
types them into the desktop screen that has focus (not while the SteamVR dashboard is
|
types them into the desktop screen that has focus (not while the SteamVR dashboard is
|
||||||
open): from keyboards that aren't grabbed, and keys a pointer device passes through.
|
open): from keyboards that aren't grabbed, and keys a pointer device passes through.
|
||||||
|
|
||||||
|
Volume keys, from every device that has them (the headset's own buttons included),
|
||||||
|
are handled here: wpctl steps the default output. Nothing else may see a volume key,
|
||||||
|
because gamescope aborts on one when no window has keyboard focus, which ends the
|
||||||
|
whole VR session. Devices with a keymap (the headset's gpio-keys, USB and Bluetooth
|
||||||
|
keyboards) get their volume entries remapped to unused stand-in codes, so their
|
||||||
|
other keys keep working for SteamVR; a device without a keymap that has only volume
|
||||||
|
keys (the headset's pmic_resin) is grabbed. The keymaps go back when the relay exits.
|
||||||
|
|
||||||
Pointer mode (POINTER=1 in ~/.config/frametop.conf) sends pointer devices to
|
Pointer mode (POINTER=1 in ~/.config/frametop.conf) sends pointer devices to
|
||||||
the ft-pointer helper (pointer/helper), which drives the ft_pointer
|
the ft-pointer helper (pointer/helper), which drives the ft_pointer
|
||||||
SteamVR driver. With POINTER=0, pointer devices go to the virtual mouse and
|
SteamVR driver. With POINTER=0, pointer devices go to the virtual mouse and
|
||||||
@@ -47,11 +55,13 @@ kernel's evdev and uinput interfaces.
|
|||||||
input-relay.py --no-grab never grab, for testing next to a running SteamVR
|
input-relay.py --no-grab never grab, for testing next to a running SteamVR
|
||||||
"""
|
"""
|
||||||
import array
|
import array
|
||||||
|
import atexit
|
||||||
import errno
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -66,6 +76,12 @@ KEY_A = 30
|
|||||||
REL_X, REL_Y, REL_WHEEL, REL_MAX = 0x00, 0x01, 0x08, 0x0F
|
REL_X, REL_Y, REL_WHEEL, REL_MAX = 0x00, 0x01, 0x08, 0x0F
|
||||||
BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA = 0x110, 0x111, 0x112, 0x113, 0x114
|
BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA = 0x110, 0x111, 0x112, 0x113, 0x114
|
||||||
KEY_LEFTMETA, KEY_RIGHTMETA = 125, 126
|
KEY_LEFTMETA, KEY_RIGHTMETA = 125, 126
|
||||||
|
KEY_VOLUMEDOWN, KEY_VOLUMEUP = 114, 115
|
||||||
|
# Volume keys are remapped to KEY_MACRO29 and KEY_MACRO30: above 255, so X11 can't
|
||||||
|
# carry them, and bound to nothing in the default keymap.
|
||||||
|
VOLUME_STANDIN = {KEY_VOLUMEUP: 0x2AC, KEY_VOLUMEDOWN: 0x2AD}
|
||||||
|
VOLUME_ORIGINAL = {v: k for k, v in VOLUME_STANDIN.items()}
|
||||||
|
VOLUME_CODES = set(VOLUME_STANDIN) | set(VOLUME_ORIGINAL)
|
||||||
BUS_USB, BUS_BLUETOOTH, BUS_VIRTUAL = 0x03, 0x05, 0x06
|
BUS_USB, BUS_BLUETOOTH, BUS_VIRTUAL = 0x03, 0x05, 0x06
|
||||||
|
|
||||||
# struct input_event on 64-bit: struct timeval (2 x long), u16 type, u16 code, s32 value.
|
# struct input_event on 64-bit: struct timeval (2 x long), u16 type, u16 code, s32 value.
|
||||||
@@ -92,6 +108,10 @@ UI_SET_KEYBIT = _iow("U", 101, 4)
|
|||||||
UI_SET_RELBIT = _iow("U", 102, 4)
|
UI_SET_RELBIT = _iow("U", 102, 4)
|
||||||
EVIOCGRAB = _iow("E", 0x90, 4)
|
EVIOCGRAB = _iow("E", 0x90, 4)
|
||||||
EVIOCGID = _ior("E", 0x02, 8)
|
EVIOCGID = _ior("E", 0x02, 8)
|
||||||
|
KEYMAP_ENTRY = struct.Struct("BBHI32s") # struct input_keymap_entry: flags, len, index, keycode, scancode
|
||||||
|
INPUT_KEYMAP_BY_INDEX = 1
|
||||||
|
EVIOCGKEYCODE_V2 = _ior("E", 0x04, KEYMAP_ENTRY.size)
|
||||||
|
EVIOCSKEYCODE_V2 = _iow("E", 0x04, KEYMAP_ENTRY.size)
|
||||||
EV_NAMES = {EV_KEY: "key", EV_REL: "rel"}
|
EV_NAMES = {EV_KEY: "key", EV_REL: "rel"}
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +237,69 @@ def read_rules(path=RULES_PATH):
|
|||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
def remap_volume(fd, restore=False):
|
||||||
|
"""Point a device's volume keys at their stand-ins in its keymap, or back with restore.
|
||||||
|
|
||||||
|
Returns how many keymap entries are volume keys or stand-ins, or None when the
|
||||||
|
device has no keymap to change (uinput devices, some platform buttons).
|
||||||
|
"""
|
||||||
|
swap = VOLUME_ORIGINAL if restore else VOLUME_STANDIN
|
||||||
|
found = 0
|
||||||
|
for index in range(8192):
|
||||||
|
entry = bytearray(KEYMAP_ENTRY.pack(INPUT_KEYMAP_BY_INDEX, 0, index, 0, b""))
|
||||||
|
try:
|
||||||
|
fcntl.ioctl(fd, EVIOCGKEYCODE_V2, entry)
|
||||||
|
except OSError:
|
||||||
|
return found if index else None # past the last entry
|
||||||
|
_, length, _, code, scancode = KEYMAP_ENTRY.unpack(entry)
|
||||||
|
if code in VOLUME_CODES:
|
||||||
|
found += 1
|
||||||
|
if code in swap:
|
||||||
|
fcntl.ioctl(fd, EVIOCSKEYCODE_V2,
|
||||||
|
KEYMAP_ENTRY.pack(INPUT_KEYMAP_BY_INDEX, length, index, swap[code], scancode))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
class Volume:
|
||||||
|
"""Volume keys: wpctl steps the default output, repeating while a key is held.
|
||||||
|
|
||||||
|
The repeat is our own, since the headset's buttons have none; kernel autorepeat
|
||||||
|
from keyboards is ignored so every device repeats the same way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
STEP = 5 # percent
|
||||||
|
DELAY, RATE = 0.4, 0.1 # seconds before repeating, and between repeats
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.held = None # (fd, code) of the key being held
|
||||||
|
self.next_at = None
|
||||||
|
|
||||||
|
def key(self, fd, code, value, now):
|
||||||
|
if value == 1:
|
||||||
|
self.held = (fd, code)
|
||||||
|
self.step(code)
|
||||||
|
self.next_at = now + self.DELAY
|
||||||
|
elif value == 0 and self.held == (fd, code):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
self.held = self.next_at = None
|
||||||
|
|
||||||
|
def step(self, code):
|
||||||
|
sign = "+" if VOLUME_ORIGINAL.get(code, code) == KEY_VOLUMEUP else "-"
|
||||||
|
subprocess.Popen(["wpctl", "set-volume", "--limit", "1.0", "@DEFAULT_AUDIO_SINK@",
|
||||||
|
f"{self.STEP}%{sign}"],
|
||||||
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def tick(self, now):
|
||||||
|
if self.next_at is not None and now >= self.next_at:
|
||||||
|
self.step(self.held[1])
|
||||||
|
self.next_at = now + self.RATE
|
||||||
|
|
||||||
|
def timeout(self, now, default):
|
||||||
|
return default if self.next_at is None else max(0.0, min(default, self.next_at - now))
|
||||||
|
|
||||||
|
|
||||||
class Pointer:
|
class Pointer:
|
||||||
"""Drives the ft_pointer SteamVR driver from a mouse (pointer mode).
|
"""Drives the ft_pointer SteamVR driver from a mouse (pointer mode).
|
||||||
|
|
||||||
@@ -373,17 +456,21 @@ class Pointer:
|
|||||||
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
"""One input event node of a candidate device (mouse or keyboard, USB or Bluetooth)."""
|
"""One input event node: a candidate device (mouse or keyboard, USB or Bluetooth),
|
||||||
|
or any other device with volume keys (candidate False, role "volume")."""
|
||||||
|
|
||||||
def __init__(self, path, fd, name, bus, vendor, product, uniq, is_mouse, is_keyboard):
|
def __init__(self, path, fd, name, bus, vendor, product, uniq, is_mouse, is_keyboard,
|
||||||
|
candidate=True, volume_keys=False, only_volume=False):
|
||||||
self.path, self.fd, self.name = path, fd, name
|
self.path, self.fd, self.name = path, fd, name
|
||||||
self.bus, self.vendor, self.product, self.uniq = bus, vendor, product, uniq
|
self.bus, self.vendor, self.product, self.uniq = bus, vendor, product, uniq
|
||||||
self.is_mouse, self.is_keyboard = is_mouse, is_keyboard
|
self.is_mouse, self.is_keyboard = is_mouse, is_keyboard
|
||||||
|
self.candidate, self.volume_keys, self.only_volume = candidate, volume_keys, only_volume
|
||||||
# One physical device, whatever its node: Bluetooth address, else USB ids plus name.
|
# One physical device, whatever its node: Bluetooth address, else USB ids plus name.
|
||||||
base = self.name.split(" Mouse")[0].split(" Keyboard")[0]
|
base = self.name.split(" Mouse")[0].split(" Keyboard")[0]
|
||||||
self.id = uniq.lower() if uniq else f"usb:{vendor:04x}:{product:04x}:{base}"
|
self.id = uniq.lower() if uniq else f"usb:{vendor:04x}:{product:04x}:{base}"
|
||||||
self.role = None
|
self.role = None if candidate else "volume"
|
||||||
self.grabbed = False
|
self.grabbed = False
|
||||||
|
self.remapped = False # volume keys remapped to their stand-ins
|
||||||
self.held = set() # keys and buttons currently down, released if the device vanishes
|
self.held = set() # keys and buttons currently down, released if the device vanishes
|
||||||
self.last_watch = 0.0
|
self.last_watch = 0.0
|
||||||
|
|
||||||
@@ -395,7 +482,8 @@ class Node:
|
|||||||
|
|
||||||
|
|
||||||
def probe(path):
|
def probe(path):
|
||||||
"""Open a node if it is a USB or Bluetooth mouse or keyboard, else return None."""
|
"""Open a node if it is a USB or Bluetooth mouse or keyboard, or has volume keys,
|
||||||
|
else return None."""
|
||||||
try:
|
try:
|
||||||
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
|
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -407,7 +495,7 @@ def probe(path):
|
|||||||
ident = bytearray(8)
|
ident = bytearray(8)
|
||||||
fcntl.ioctl(fd, EVIOCGID, ident)
|
fcntl.ioctl(fd, EVIOCGID, ident)
|
||||||
bus, vendor, product, _ = struct.unpack("HHHH", ident)
|
bus, vendor, product, _ = struct.unpack("HHHH", ident)
|
||||||
if name.startswith(VIRTUAL_PREFIX) or bus not in (BUS_USB, BUS_BLUETOOTH):
|
if name.startswith(VIRTUAL_PREFIX):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
uniq_buf = bytearray(64)
|
uniq_buf = bytearray(64)
|
||||||
try:
|
try:
|
||||||
@@ -415,11 +503,15 @@ def probe(path):
|
|||||||
uniq = uniq_buf.split(b"\0", 1)[0].decode(errors="replace")
|
uniq = uniq_buf.split(b"\0", 1)[0].decode(errors="replace")
|
||||||
except OSError:
|
except OSError:
|
||||||
uniq = ""
|
uniq = ""
|
||||||
|
keys = bits(fd, EV_KEY, KEY_MAX + 1)
|
||||||
is_mouse = REL_X in bits(fd, EV_REL, REL_MAX + 1)
|
is_mouse = REL_X in bits(fd, EV_REL, REL_MAX + 1)
|
||||||
is_keyboard = KEY_A in bits(fd, EV_KEY, KEY_MAX + 1)
|
is_keyboard = KEY_A in keys
|
||||||
if not (is_mouse or is_keyboard):
|
candidate = bus in (BUS_USB, BUS_BLUETOOTH) and (is_mouse or is_keyboard)
|
||||||
|
volume_keys = bool(keys & VOLUME_CODES) # stand-ins too: kept from before a relay restart
|
||||||
|
if not (candidate or volume_keys):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
return Node(path, fd, name, bus, vendor, product, uniq, is_mouse, is_keyboard)
|
return Node(path, fd, name, bus, vendor, product, uniq, is_mouse, is_keyboard,
|
||||||
|
candidate, volume_keys, keys <= VOLUME_CODES)
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
return None
|
return None
|
||||||
@@ -474,6 +566,50 @@ def main():
|
|||||||
seen = {}
|
seen = {}
|
||||||
next_scan = 0.0
|
next_scan = 0.0
|
||||||
|
|
||||||
|
volume = Volume()
|
||||||
|
|
||||||
|
def restore_keymaps():
|
||||||
|
"""Give remapped devices their volume keys back, so they work without the relay."""
|
||||||
|
for node in nodes.values():
|
||||||
|
if node.remapped:
|
||||||
|
try:
|
||||||
|
remap_volume(node.fd, restore=True)
|
||||||
|
except OSError:
|
||||||
|
pass # device already gone
|
||||||
|
|
||||||
|
atexit.register(restore_keymaps)
|
||||||
|
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) # so atexit runs on systemctl stop
|
||||||
|
|
||||||
|
def take_volume(node):
|
||||||
|
"""Keep the node's volume keys from SteamVR (and so from gamescope).
|
||||||
|
|
||||||
|
Returns False for a non-candidate node there's nothing to do with.
|
||||||
|
"""
|
||||||
|
if not can_grab:
|
||||||
|
return node.candidate
|
||||||
|
try:
|
||||||
|
found = remap_volume(node.fd)
|
||||||
|
except OSError as e:
|
||||||
|
log(f"remapping volume keys failed for {node.name}: {e}")
|
||||||
|
# Some entries may have changed already: handle their stand-ins and restore them.
|
||||||
|
node.remapped = True
|
||||||
|
found = None
|
||||||
|
if found:
|
||||||
|
node.remapped = True
|
||||||
|
log(f"{node.name} ({node.path}): volume keys taken over (remapped)")
|
||||||
|
return True
|
||||||
|
if found is None and node.only_volume:
|
||||||
|
try:
|
||||||
|
fcntl.ioctl(node.fd, EVIOCGRAB, 1)
|
||||||
|
node.grabbed = True
|
||||||
|
log(f"{node.name} ({node.path}): volume keys taken over (grabbed)")
|
||||||
|
return True
|
||||||
|
except OSError as e:
|
||||||
|
log(f"grab failed for {node.name}: {e}")
|
||||||
|
# Grabbed pointer devices still have their volume keys handled here.
|
||||||
|
log(f"{node.name} ({node.path}): can't take over its volume keys")
|
||||||
|
return node.candidate
|
||||||
|
|
||||||
def role_of(node):
|
def role_of(node):
|
||||||
rule = state["rules"]["devices"].get(node.id, {})
|
rule = state["rules"]["devices"].get(node.id, {})
|
||||||
if rule.get("role") in ("pointer", "passthrough", "ignore"):
|
if rule.get("role") in ("pointer", "passthrough", "ignore"):
|
||||||
@@ -490,6 +626,8 @@ def main():
|
|||||||
|
|
||||||
def apply_roles():
|
def apply_roles():
|
||||||
for node in nodes.values():
|
for node in nodes.values():
|
||||||
|
if not node.candidate:
|
||||||
|
continue # volume keys only, taken over when found
|
||||||
role = role_of(node)
|
role = role_of(node)
|
||||||
want_grab = can_grab and role == "pointer"
|
want_grab = can_grab and role == "pointer"
|
||||||
if want_grab != node.grabbed:
|
if want_grab != node.grabbed:
|
||||||
@@ -506,6 +644,8 @@ def main():
|
|||||||
|
|
||||||
def drop(node, reason):
|
def drop(node, reason):
|
||||||
release_held(node)
|
release_held(node)
|
||||||
|
if volume.held and volume.held[0] == node.fd:
|
||||||
|
volume.release()
|
||||||
os.close(node.fd)
|
os.close(node.fd)
|
||||||
del nodes[node.fd]
|
del nodes[node.fd]
|
||||||
seen.pop(node.path, None)
|
seen.pop(node.path, None)
|
||||||
@@ -529,7 +669,8 @@ def main():
|
|||||||
cmd = words[0] if words else ""
|
cmd = words[0] if words else ""
|
||||||
if cmd == "devices":
|
if cmd == "devices":
|
||||||
reply(addr, {"t": "devices", "pointer_mode": state["pointer"] is not None,
|
reply(addr, {"t": "devices", "pointer_mode": state["pointer"] is not None,
|
||||||
"actions": ACTIONS, "nodes": [n.describe() for n in nodes.values()]})
|
"actions": ACTIONS,
|
||||||
|
"nodes": [n.describe() for n in nodes.values() if n.candidate]})
|
||||||
elif cmd == "watch":
|
elif cmd == "watch":
|
||||||
seconds = float(words[1]) if len(words) > 1 else 30
|
seconds = float(words[1]) if len(words) > 1 else 30
|
||||||
watchers[addr] = now + min(seconds, 600)
|
watchers[addr] = now + min(seconds, 600)
|
||||||
@@ -544,7 +685,7 @@ def main():
|
|||||||
reply(addr, {"t": "error", "error": f"unknown command {cmd!r}"})
|
reply(addr, {"t": "error", "error": f"unknown command {cmd!r}"})
|
||||||
|
|
||||||
def broadcast(node, etype, code, value, now):
|
def broadcast(node, etype, code, value, now):
|
||||||
if not watchers:
|
if not watchers or not node.candidate:
|
||||||
return
|
return
|
||||||
if etype == EV_REL and now - node.last_watch < 0.05:
|
if etype == EV_REL and now - node.last_watch < 0.05:
|
||||||
return # motion: enough for an activity light
|
return # motion: enough for an activity light
|
||||||
@@ -580,6 +721,9 @@ def main():
|
|||||||
drop(old, "replaced by a new device node")
|
drop(old, "replaced by a new device node")
|
||||||
seen[path] = ino
|
seen[path] = ino
|
||||||
node = probe(path)
|
node = probe(path)
|
||||||
|
if node and node.volume_keys and not take_volume(node):
|
||||||
|
os.close(node.fd)
|
||||||
|
node = None
|
||||||
if node:
|
if node:
|
||||||
nodes[node.fd] = node
|
nodes[node.fd] = node
|
||||||
added = True
|
added = True
|
||||||
@@ -587,10 +731,11 @@ def main():
|
|||||||
apply_roles()
|
apply_roles()
|
||||||
|
|
||||||
ready, _, _ = select.select(list(nodes) + [control], [], [],
|
ready, _, _ = select.select(list(nodes) + [control], [], [],
|
||||||
pointer.timeout() if pointer else 0.5)
|
volume.timeout(now, pointer.timeout() if pointer else 0.5))
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if pointer:
|
if pointer:
|
||||||
pointer.tick(now)
|
pointer.tick(now)
|
||||||
|
volume.tick(now)
|
||||||
for fd in ready:
|
for fd in ready:
|
||||||
if fd is control:
|
if fd is control:
|
||||||
handle_control(now)
|
handle_control(now)
|
||||||
@@ -610,7 +755,16 @@ def main():
|
|||||||
for off in range(0, len(data) - EVENT.size + 1, EVENT.size):
|
for off in range(0, len(data) - EVENT.size + 1, EVENT.size):
|
||||||
_, _, etype, code, value = EVENT.unpack_from(data, off)
|
_, _, etype, code, value = EVENT.unpack_from(data, off)
|
||||||
if etype in (EV_KEY, EV_REL):
|
if etype in (EV_KEY, EV_REL):
|
||||||
broadcast(node, etype, code, value, now)
|
broadcast(node, etype, VOLUME_ORIGINAL.get(code, code) if node.remapped else code,
|
||||||
|
value, now)
|
||||||
|
if etype == EV_KEY and ((node.remapped and code in VOLUME_ORIGINAL)
|
||||||
|
or (node.grabbed and code in VOLUME_STANDIN)):
|
||||||
|
volume.key(fd, code, value, now)
|
||||||
|
if value == 1:
|
||||||
|
meta_down = False # Meta used as a modifier, not a tap
|
||||||
|
continue
|
||||||
|
if node.role == "volume":
|
||||||
|
continue
|
||||||
if node.role != "pointer":
|
if node.role != "pointer":
|
||||||
# Observed only. With META_DASHBOARD=1, a Meta tap on any keyboard
|
# Observed only. With META_DASHBOARD=1, a Meta tap on any keyboard
|
||||||
# toggles the dashboard.
|
# toggles the dashboard.
|
||||||
|
|||||||
Reference in new issue
Block a user