quickshell: keychron mouse battery in bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
54f7c1123b
commit
e18ee80a1a
2 changed files with 102 additions and 0 deletions
|
|
@ -36,10 +36,13 @@
|
||||||
# Nova Pro Wireless base station: open up its vendor HID interface so
|
# Nova Pro Wireless base station: open up its vendor HID interface so
|
||||||
# quickshell's ANC switcher can write to it directly (nova-anc script in
|
# quickshell's ANC switcher can write to it directly (nova-anc script in
|
||||||
# settings/quickshell.nix). Single-user desktop, 0666 is fine.
|
# settings/quickshell.nix). Single-user desktop, 0666 is fine.
|
||||||
|
# Keychron (receiver + wired mouse) likewise for the bar's mouse-battery
|
||||||
|
# readout; also lets the Keychron Launcher's WebHID reach it.
|
||||||
services.udev.extraRules = ''
|
services.udev.extraRules = ''
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e0", MODE="0666"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e0", MODE="0666"
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e5", MODE="0666"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12e5", MODE="0666"
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="225d", MODE="0666"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="1038", ATTRS{idProduct}=="225d", MODE="0666"
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="3434", MODE="0666"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Force heavy builds off the gaming PC onto the media server so nix updates
|
# Force heavy builds off the gaming PC onto the media server so nix updates
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,45 @@ in
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
'';
|
'';
|
||||||
|
# Keychron mouse battery, straight off the vendor HID interface
|
||||||
|
# (usage page 0xffc1) of the Ultra-Link receiver or a wired mouse.
|
||||||
|
# Report 0xb3 with cmd 0x06 asks for base info; the 0xb4 reply has
|
||||||
|
# battery at data byte 19 (low 7 bits %, bit 7 charging).
|
||||||
|
# Protocol lifted from the Keychron Launcher's WebHID bundle.
|
||||||
|
mouseBatteryScript = pkgs.writeScript "mouse-battery" ''
|
||||||
|
#!${pkgs.python3}/bin/python3
|
||||||
|
"""mouse-battery — prints "<pct>" or "<pct> charging"."""
|
||||||
|
import glob, os, select, sys, time
|
||||||
|
|
||||||
|
def iface():
|
||||||
|
for h in glob.glob("/sys/class/hidraw/hidraw*"):
|
||||||
|
try:
|
||||||
|
uevent = open(h + "/device/uevent").read()
|
||||||
|
desc = open(h + "/device/report_descriptor", "rb").read(3)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if ":00003434:" in uevent and desc == b"\x06\xc1\xff":
|
||||||
|
return "/dev/" + os.path.basename(h)
|
||||||
|
return None
|
||||||
|
|
||||||
|
dev = iface()
|
||||||
|
if not dev:
|
||||||
|
sys.exit(1)
|
||||||
|
fd = os.open(dev, os.O_RDWR)
|
||||||
|
os.write(fd, bytes([0xb3, 0x06]).ljust(64, b"\0"))
|
||||||
|
deadline = time.time() + 1.0
|
||||||
|
while (left := deadline - time.time()) > 0:
|
||||||
|
if not select.select([fd], [], [], left)[0]:
|
||||||
|
break
|
||||||
|
pkt = os.read(fd, 64)
|
||||||
|
if len(pkt) > 20 and pkt[0] == 0xb4 and pkt[1] == 0x06:
|
||||||
|
pct = pkt[20] & 0x7f
|
||||||
|
if pct == 0:
|
||||||
|
break # mouse off / asleep
|
||||||
|
print(f"{pct} charging" if pkt[20] & 0x80 else pct)
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(1)
|
||||||
|
'';
|
||||||
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
weatherFetchScript = pkgs.writeShellScript "weather-fetch" ''
|
||||||
loc=$(${pkgs.curl}/bin/curl -sf --max-time 5 https://ipinfo.io/loc 2>/dev/null || true)
|
loc=$(${pkgs.curl}/bin/curl -sf --max-time 5 https://ipinfo.io/loc 2>/dev/null || true)
|
||||||
case "$loc" in
|
case "$loc" in
|
||||||
|
|
@ -880,6 +919,7 @@ in
|
||||||
readonly property string weatherFetch: "${weatherFetchScript}"
|
readonly property string weatherFetch: "${weatherFetchScript}"
|
||||||
readonly property string ssh: "${pkgs.openssh}/bin/ssh"
|
readonly property string ssh: "${pkgs.openssh}/bin/ssh"
|
||||||
readonly property string novaAnc: "${novaAncScript}"
|
readonly property string novaAnc: "${novaAncScript}"
|
||||||
|
readonly property string mouseBattery: "${mouseBatteryScript}"
|
||||||
// Unprivileged ping works via ICMP dgram sockets — NixOS sets
|
// Unprivileged ping works via ICMP dgram sockets — NixOS sets
|
||||||
// net.ipv4.ping_group_range for everyone (no wrapper exists).
|
// net.ipv4.ping_group_range for everyone (no wrapper exists).
|
||||||
readonly property string ping: "${pkgs.iputils}/bin/ping"
|
readonly property string ping: "${pkgs.iputils}/bin/ping"
|
||||||
|
|
@ -4065,6 +4105,65 @@ in
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
|
|
||||||
|
${lib.optionalString (!isMacbook) ''
|
||||||
|
// Wireless mouse battery — polled off the Keychron
|
||||||
|
// receiver via the mouse-battery script. Hidden while the
|
||||||
|
// mouse is off/asleep or the receiver is unplugged.
|
||||||
|
Item {
|
||||||
|
id: mouseBatWidget
|
||||||
|
visible: level > 0
|
||||||
|
width: Theme.barWidth
|
||||||
|
height: mouseBatCol.height
|
||||||
|
|
||||||
|
property int level: 0
|
||||||
|
property bool charging: false
|
||||||
|
property color tint: charging ? Theme.accent
|
||||||
|
: level <= 15 ? Theme.err
|
||||||
|
: level <= 30 ? Theme.warn
|
||||||
|
: Theme.base05
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: mouseBatProc
|
||||||
|
command: [Commands.mouseBattery]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const f = text.trim().split(" ");
|
||||||
|
mouseBatWidget.level = parseInt(f[0]) || 0;
|
||||||
|
mouseBatWidget.charging = f[1] === "charging";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: mouseBatProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mouseBatCol
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
SIcon {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "mouse"
|
||||||
|
color: mouseBatWidget.tint
|
||||||
|
font.pixelSize: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
SText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: mouseBatWidget.level
|
||||||
|
color: mouseBatWidget.tint
|
||||||
|
font.pixelSize: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
|
||||||
// Quick settings — present on every host, so the shared
|
// Quick settings — present on every host, so the shared
|
||||||
// controls sit in the same place on all of them.
|
// controls sit in the same place on all of them.
|
||||||
Item {
|
Item {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue