Open the app with the Predator key, bump to 1.3.0
The Predator key sends keyboard usage 0xC9, which lies outside the logical range the keyboard declares, so the kernel never emits an input event for it. It is also reported on vendor report 4 of USB interface 2 (04 81 ff on press). predatord now listens there and opens the app in the active graphical session via the user's systemd manager. The action is configurable on the System page: open the app, switch to the next performance mode, or do nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
|
||||
__version__ = "1.2.0"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
APP_NAME = "Predator Control"
|
||||
RUN_DIR = os.environ.get("PREDATOR_CONTROL_RUN_DIR", "/run/predator-control")
|
||||
|
||||
@@ -25,6 +25,7 @@ import traceback
|
||||
from . import CONFIG_DIR, CONFIG_PATH, SOCKET_PATH, STATE_PATH, __version__
|
||||
from . import hardware as hw
|
||||
from . import updater
|
||||
from .hotkey import HotkeyListener, launch_in_session
|
||||
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -49,6 +50,7 @@ DEFAULT_CONFIG = {
|
||||
"battery": {"limit": None}, # None = leave the firmware state alone
|
||||
"keyboard_presets": {},
|
||||
"updates": {"auto_check": True, "interval_hours": 6},
|
||||
"predator_key": "open", # open | cycle_profile | none
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +279,18 @@ class Daemon:
|
||||
self._last_try = time.time()
|
||||
self.check_update_async()
|
||||
|
||||
def predator_key(self):
|
||||
action = self.config.get("predator_key", "open")
|
||||
if action == "open":
|
||||
if not launch_in_session(["predator-control"]):
|
||||
log("Predator key: no active graphical session to open the app in")
|
||||
elif action == "cycle_profile":
|
||||
with self.lock:
|
||||
ch = hw.profile_choices()
|
||||
cur = hw.get_profile()
|
||||
nxt = ch[(ch.index(cur) + 1) % len(ch)] if cur in ch else ch[0]
|
||||
self._try("profile", hw.set_profile, nxt)
|
||||
|
||||
def loop(self):
|
||||
while True:
|
||||
try:
|
||||
@@ -328,6 +342,12 @@ class Daemon:
|
||||
merge(self.config["updates"], {k: a[k] for k in ("auto_check", "interval_hours") if k in a})
|
||||
self.save_config()
|
||||
return {}
|
||||
if cmd == "set_predator_key":
|
||||
if a["action"] not in ("open", "cycle_profile", "none"):
|
||||
raise ValueError("unknown action")
|
||||
self.config["predator_key"] = a["action"]
|
||||
self.save_config()
|
||||
return {}
|
||||
if cmd == "restore":
|
||||
self.restore()
|
||||
return {}
|
||||
@@ -511,6 +531,7 @@ def main():
|
||||
signal.signal(signal.SIGTERM, lambda *_: (server.server_close(), sys.exit(0)))
|
||||
signal.signal(signal.SIGUSR1, lambda *_: threading.Thread(target=lambda: (d.lock.acquire(), d.restore(), d.lock.release())).start())
|
||||
threading.Thread(target=d.loop, daemon=True).start()
|
||||
HotkeyListener(d.predator_key, log).start()
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
|
||||
@@ -674,6 +674,19 @@ class SystemPage(Page):
|
||||
self.fw_card.body.addWidget(_muted("Values are stored in the laptop firmware (EC/BIOS)."))
|
||||
self.lay.addWidget(self.fw_card)
|
||||
|
||||
key = Card("Predator key")
|
||||
kr = QHBoxLayout()
|
||||
kr.addWidget(QLabel("When the Predator key is pressed"))
|
||||
self.key_action = QComboBox()
|
||||
for val, text in (("open", "Open Predator Control"), ("cycle_profile", "Switch to the next performance mode"),
|
||||
("none", "Do nothing")):
|
||||
self.key_action.addItem(text, val)
|
||||
self.key_action.activated.connect(lambda: win.backend.run("set_predator_key", action=self.key_action.currentData()))
|
||||
kr.addWidget(self.key_action)
|
||||
kr.addStretch(1)
|
||||
key.body.addLayout(kr)
|
||||
self.lay.addWidget(key)
|
||||
|
||||
upd = Card("Updates")
|
||||
self.upd_head = QLabel("")
|
||||
self.upd_head.setObjectName("big")
|
||||
@@ -782,6 +795,8 @@ class SystemPage(Page):
|
||||
err = ("\n\n⚠ %s" % st["update_error"]) if st.get("update_error") else ""
|
||||
self.upd_text.setText("Installed version %s · last check %s%s%s" % (st.get("version"), when, err, extra))
|
||||
self.upd_install.hide()
|
||||
if not self.key_action.view().isVisible():
|
||||
self.key_action.setCurrentIndex(max(0, self.key_action.findData(st["config"].get("predator_key", "open"))))
|
||||
self.upd_auto.setChecked(st["config"].get("updates", {}).get("auto_check", True))
|
||||
a = st["available"]
|
||||
yes = lambda v: "✔" if v else "✘"
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Predator key listener.
|
||||
|
||||
The PH16-71 Predator key produces no input event: its keyboard usage (0xC9) is outside the
|
||||
range the keyboard declares, so the kernel drops it. The key is also reported on the
|
||||
vendor report 4 of USB interface 2: `04 81 ff` on press, `04 00 00` on release.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import select
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .keyboard import HOTKEY_INTERFACE, find_hidraw
|
||||
|
||||
PRESS_PREFIX = b"\x04\x81"
|
||||
|
||||
|
||||
def active_session_user():
|
||||
"""(uid, name) of the active graphical session, or None."""
|
||||
out = subprocess.run(["loginctl", "list-sessions", "--no-legend"],
|
||||
capture_output=True, text=True).stdout
|
||||
for line in out.splitlines():
|
||||
sid = line.split()[0] if line.split() else None
|
||||
if not sid:
|
||||
continue
|
||||
props = dict(l.split("=", 1) for l in subprocess.run(
|
||||
["loginctl", "show-session", sid, "-p", "Active", "-p", "Type", "-p", "User", "-p", "Name"],
|
||||
capture_output=True, text=True).stdout.splitlines() if "=" in l)
|
||||
if props.get("Active") == "yes" and props.get("Type") in ("wayland", "x11"):
|
||||
return int(props["User"]), props["Name"]
|
||||
return None
|
||||
|
||||
|
||||
def launch_in_session(args):
|
||||
sess = active_session_user()
|
||||
if not sess:
|
||||
return False
|
||||
uid, name = sess
|
||||
# runs in the user's systemd manager, which carries WAYLAND_DISPLAY/DISPLAY on Plasma
|
||||
r = subprocess.run(["systemd-run", "--quiet", "--user", "--machine=%s@.host" % name, "--collect",
|
||||
"--unit=predator-control-launch-%d" % int(time.time() * 1000)] + args,
|
||||
capture_output=True, text=True)
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
class HotkeyListener(threading.Thread):
|
||||
def __init__(self, on_press, log):
|
||||
super().__init__(daemon=True)
|
||||
self.on_press = on_press
|
||||
self.log = log
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
path = find_hidraw(HOTKEY_INTERFACE)
|
||||
if not path:
|
||||
time.sleep(5)
|
||||
continue
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY)
|
||||
except OSError as e:
|
||||
self.log("hotkey: cannot open %s: %s" % (path, e))
|
||||
time.sleep(5)
|
||||
continue
|
||||
last = 0.0
|
||||
try:
|
||||
while True:
|
||||
select.select([fd], [], [])
|
||||
data = os.read(fd, 64)
|
||||
if not data:
|
||||
break
|
||||
if data.startswith(PRESS_PREFIX) and time.monotonic() - last > 0.4:
|
||||
last = time.monotonic()
|
||||
try:
|
||||
self.on_press()
|
||||
except Exception as e:
|
||||
self.log("hotkey: %s" % e)
|
||||
except OSError:
|
||||
pass # device vanished (suspend/replug) – reopen
|
||||
finally:
|
||||
os.close(fd)
|
||||
time.sleep(1)
|
||||
@@ -13,6 +13,7 @@ import threading
|
||||
ACER_VID = 0x04F2
|
||||
KNOWN_PIDS = (0x0117, 0x011A)
|
||||
LED_INTERFACE = 3
|
||||
HOTKEY_INTERFACE = 2 # vendor report 4 carries the Predator key
|
||||
NUM_CELLS = 128
|
||||
|
||||
OP_BEGIN = 0x88
|
||||
@@ -57,7 +58,7 @@ def checksum(body7):
|
||||
return (0xFF - (sum(body7) & 0xFF)) & 0xFF
|
||||
|
||||
|
||||
def find_hidraw():
|
||||
def find_hidraw(interface=LED_INTERFACE):
|
||||
"""Path of the hidraw node of the LED interface, or None."""
|
||||
for node in sorted(glob.glob("/sys/class/hidraw/hidraw*")):
|
||||
try:
|
||||
@@ -76,7 +77,7 @@ def find_hidraw():
|
||||
devpath = os.path.realpath(os.path.join(node, "device"))
|
||||
# .../1-7:1.3/0003:04F2:011A.0004 -> interface number from the parent
|
||||
iface = os.path.basename(os.path.dirname(devpath)).rsplit(".", 1)[-1]
|
||||
if iface == str(LED_INTERFACE):
|
||||
if iface == str(interface):
|
||||
return "/dev/" + os.path.basename(node)
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user