Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b7534560f |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.3.0 – 2026-10-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Predator key opens the app.** The key produces no input event on Linux (its usage code is
|
||||||
|
outside the range the keyboard declares), so predatord listens on the keyboard's vendor HID
|
||||||
|
report instead and opens Predator Control in the active desktop session
|
||||||
|
- Choose the key's action on the System page: open the app, switch to the next performance mode,
|
||||||
|
or do nothing
|
||||||
|
|
||||||
## 1.2.0 – 2026-10-07
|
## 1.2.0 – 2026-10-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ Keyboard RGB and performance profiles work without the module.
|
|||||||
|
|
||||||
Uninstall with `sudo pacman -R predator-control` (package) or `sudo ./uninstall.sh` (add `--purge` to also delete your settings).
|
Uninstall with `sudo pacman -R predator-control` (package) or `sudo ./uninstall.sh` (add `--purge` to also delete your settings).
|
||||||
|
|
||||||
|
## Predator key
|
||||||
|
|
||||||
|
Pressing the **Predator key** opens Predator Control, just like PredatorSense on Windows.
|
||||||
|
On the **System** page you can instead make it switch to the next performance mode, or disable it.
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
Installed via the pacman package, Predator Control updates itself: the service checks for new
|
Installed via the pacman package, Predator Control updates itself: the service checks for new
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# sudo ./install.sh --no-deps skip package installation (non-Arch distros)
|
# sudo ./install.sh --no-deps skip package installation (non-Arch distros)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="1.2.0"
|
VERSION="1.3.0"
|
||||||
MODULE="predator-wmi"
|
MODULE="predator-wmi"
|
||||||
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PREFIX="/usr"
|
PREFIX="/usr"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
PACKAGE_NAME="predator-wmi"
|
PACKAGE_NAME="predator-wmi"
|
||||||
PACKAGE_VERSION="1.2.0"
|
PACKAGE_VERSION="1.3.0"
|
||||||
BUILT_MODULE_NAME[0]="predator_wmi"
|
BUILT_MODULE_NAME[0]="predator_wmi"
|
||||||
DEST_MODULE_LOCATION[0]="/kernel/drivers/platform/x86"
|
DEST_MODULE_LOCATION[0]="/kernel/drivers/platform/x86"
|
||||||
MAKE[0]="make KVER=${kernelver} KDIR=${kernel_source_dir}"
|
MAKE[0]="make KVER=${kernelver} KDIR=${kernel_source_dir}"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# sudo pacman -U predator-control-*.pkg.tar.zst
|
# sudo pacman -U predator-control-*.pkg.tar.zst
|
||||||
|
|
||||||
pkgname=predator-control
|
pkgname=predator-control
|
||||||
pkgver=1.2.0
|
pkgver=1.3.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="PredatorSense replacement for Acer Predator laptops: performance modes, fans, keyboard RGB, lightbar, battery (Qt app + Plasma widget)"
|
pkgdesc="PredatorSense replacement for Acer Predator laptops: performance modes, fans, keyboard RGB, lightbar, battery (Qt app + Plasma widget)"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@@ -22,7 +22,7 @@ install=predator-control.install
|
|||||||
source=()
|
source=()
|
||||||
|
|
||||||
_src="$(realpath "$startdir/../..")"
|
_src="$(realpath "$startdir/../..")"
|
||||||
_modver=1.2.0
|
_modver=1.3.0
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$_src"
|
cd "$_src"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"Id": "org.predatorcontrol.panel",
|
"Id": "org.predatorcontrol.panel",
|
||||||
"License": "GPL-2.0-only",
|
"License": "GPL-2.0-only",
|
||||||
"Name": "Predator Control",
|
"Name": "Predator Control",
|
||||||
"Version": "1.2.0",
|
"Version": "1.3.0",
|
||||||
"Website": "https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard"
|
"Website": "https://git.mrblake.cc/MrBlake/Cachy-Predator-Keyboard"
|
||||||
},
|
},
|
||||||
"X-Plasma-API-Minimum-Version": "6.0",
|
"X-Plasma-API-Minimum-Version": "6.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
__version__ = "1.2.0"
|
__version__ = "1.3.0"
|
||||||
|
|
||||||
APP_NAME = "Predator Control"
|
APP_NAME = "Predator Control"
|
||||||
RUN_DIR = os.environ.get("PREDATOR_CONTROL_RUN_DIR", "/run/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 CONFIG_DIR, CONFIG_PATH, SOCKET_PATH, STATE_PATH, __version__
|
||||||
from . import hardware as hw
|
from . import hardware as hw
|
||||||
from . import updater
|
from . import updater
|
||||||
|
from .hotkey import HotkeyListener, launch_in_session
|
||||||
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
|
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
@@ -49,6 +50,7 @@ DEFAULT_CONFIG = {
|
|||||||
"battery": {"limit": None}, # None = leave the firmware state alone
|
"battery": {"limit": None}, # None = leave the firmware state alone
|
||||||
"keyboard_presets": {},
|
"keyboard_presets": {},
|
||||||
"updates": {"auto_check": True, "interval_hours": 6},
|
"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._last_try = time.time()
|
||||||
self.check_update_async()
|
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):
|
def loop(self):
|
||||||
while True:
|
while True:
|
||||||
try:
|
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})
|
merge(self.config["updates"], {k: a[k] for k in ("auto_check", "interval_hours") if k in a})
|
||||||
self.save_config()
|
self.save_config()
|
||||||
return {}
|
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":
|
if cmd == "restore":
|
||||||
self.restore()
|
self.restore()
|
||||||
return {}
|
return {}
|
||||||
@@ -511,6 +531,7 @@ def main():
|
|||||||
signal.signal(signal.SIGTERM, lambda *_: (server.server_close(), sys.exit(0)))
|
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())
|
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()
|
threading.Thread(target=d.loop, daemon=True).start()
|
||||||
|
HotkeyListener(d.predator_key, log).start()
|
||||||
server.serve_forever()
|
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.fw_card.body.addWidget(_muted("Values are stored in the laptop firmware (EC/BIOS)."))
|
||||||
self.lay.addWidget(self.fw_card)
|
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")
|
upd = Card("Updates")
|
||||||
self.upd_head = QLabel("")
|
self.upd_head = QLabel("")
|
||||||
self.upd_head.setObjectName("big")
|
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 ""
|
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_text.setText("Installed version %s · last check %s%s%s" % (st.get("version"), when, err, extra))
|
||||||
self.upd_install.hide()
|
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))
|
self.upd_auto.setChecked(st["config"].get("updates", {}).get("auto_check", True))
|
||||||
a = st["available"]
|
a = st["available"]
|
||||||
yes = lambda v: "✔" if v else "✘"
|
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
|
ACER_VID = 0x04F2
|
||||||
KNOWN_PIDS = (0x0117, 0x011A)
|
KNOWN_PIDS = (0x0117, 0x011A)
|
||||||
LED_INTERFACE = 3
|
LED_INTERFACE = 3
|
||||||
|
HOTKEY_INTERFACE = 2 # vendor report 4 carries the Predator key
|
||||||
NUM_CELLS = 128
|
NUM_CELLS = 128
|
||||||
|
|
||||||
OP_BEGIN = 0x88
|
OP_BEGIN = 0x88
|
||||||
@@ -57,7 +58,7 @@ def checksum(body7):
|
|||||||
return (0xFF - (sum(body7) & 0xFF)) & 0xFF
|
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."""
|
"""Path of the hidraw node of the LED interface, or None."""
|
||||||
for node in sorted(glob.glob("/sys/class/hidraw/hidraw*")):
|
for node in sorted(glob.glob("/sys/class/hidraw/hidraw*")):
|
||||||
try:
|
try:
|
||||||
@@ -76,7 +77,7 @@ def find_hidraw():
|
|||||||
devpath = os.path.realpath(os.path.join(node, "device"))
|
devpath = os.path.realpath(os.path.join(node, "device"))
|
||||||
# .../1-7:1.3/0003:04F2:011A.0004 -> interface number from the parent
|
# .../1-7:1.3/0003:04F2:011A.0004 -> interface number from the parent
|
||||||
iface = os.path.basename(os.path.dirname(devpath)).rsplit(".", 1)[-1]
|
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 "/dev/" + os.path.basename(node)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user