quickshell: headset battery next to mouse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
434db62cd6
commit
fa1ed741fc
1 changed files with 101 additions and 56 deletions
|
|
@ -233,9 +233,12 @@ in
|
|||
# packet; opcode 0xbd sets ANC (0 off / 1 transparent / 2 on), 0xb0
|
||||
# requests full status (anc @ 0x0a, power @ 0x0f, 0x08 = on-head).
|
||||
# Protocol lifted from Arctis-Sound-Manager's nova_pro_wireless.yaml.
|
||||
# Battery is 0x06 in the same reply (0-8 scale); power 0x01 = off,
|
||||
# 0x02 = cable charging (HeadsetControl's nova_pro_wireless driver).
|
||||
novaAncScript = pkgs.writeScript "nova-anc" ''
|
||||
#!${pkgs.python3}/bin/python3
|
||||
"""nova-anc [off|transparent|on] — no arg prints current mode."""
|
||||
"""nova-anc [off|transparent|on] — no arg prints current mode.
|
||||
nova-anc battery — prints "<pct>" or "<pct> charging"."""
|
||||
import glob, os, select, sys, time
|
||||
|
||||
MODES = {"off": 0, "transparent": 1, "on": 2}
|
||||
|
|
@ -261,7 +264,8 @@ in
|
|||
sys.exit(1)
|
||||
fd = os.open(dev, os.O_RDWR)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
battery = sys.argv[1:] == ["battery"]
|
||||
if len(sys.argv) > 1 and not battery:
|
||||
os.write(fd, bytes([0x06, 0xbd, MODES[sys.argv[1]]]).ljust(64, b"\0"))
|
||||
print(sys.argv[1])
|
||||
sys.exit(0)
|
||||
|
|
@ -274,6 +278,12 @@ in
|
|||
pkt = os.read(fd, 64)
|
||||
# Skip unsolicited 0x07xx event packets until the status reply
|
||||
if len(pkt) > 0x0f and pkt[0] == 0x06 and pkt[1] == 0xb0:
|
||||
if battery:
|
||||
if pkt[0x0f] == 0x01:
|
||||
break # headset off
|
||||
pct = round(pkt[0x06] * 100 / 8)
|
||||
print(f"{pct} charging" if pkt[0x0f] == 0x02 else pct)
|
||||
sys.exit(0)
|
||||
if pkt[0x0f] != 0x08:
|
||||
break # headset off / in cradle -> report nothing
|
||||
print(NAMES.get(pkt[0x0a], "off"))
|
||||
|
|
@ -1505,6 +1515,60 @@ in
|
|||
}
|
||||
}
|
||||
|
||||
// ── BatteryRow: icon + label on the left, battery glyph and
|
||||
// percentage on the right, tinted by level. Hidden while
|
||||
// `level` is 0 (device off or unreadable).
|
||||
component BatteryRow: Item {
|
||||
id: _br
|
||||
property string icon
|
||||
property string label
|
||||
property int level: 0
|
||||
property bool charging: false
|
||||
readonly property color tint: charging ? Theme.accent
|
||||
: level <= 15 ? Theme.err
|
||||
: level <= 30 ? Theme.warn
|
||||
: Theme.base05
|
||||
visible: level > 0
|
||||
height: 20
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: _br.icon
|
||||
font.pixelSize: 15
|
||||
}
|
||||
SText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: _br.label
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: _br.charging ? "battery-charging"
|
||||
: _br.level >= 75 ? "battery-full"
|
||||
: _br.level >= 50 ? "battery-medium"
|
||||
: _br.level >= 20 ? "battery-low"
|
||||
: "battery"
|
||||
color: _br.tint
|
||||
font.pixelSize: 15
|
||||
}
|
||||
SText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: _br.level + "%"
|
||||
color: _br.tint
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── PillSlider: rounded track + fill + knob with a full-height
|
||||
// invisible hit area. `value` is 0..1; `moved(v)` fires on drag.
|
||||
// Knob diameter derives from the track, so the small app-stream
|
||||
|
|
@ -5475,30 +5539,36 @@ in
|
|||
spacing: 8
|
||||
|
||||
${lib.optionalString (!isMacbook) ''
|
||||
// Wireless mouse battery, read off the Keychron
|
||||
// receiver by the mouse-battery script. Polls only
|
||||
// while the panel is up; hidden while the mouse is
|
||||
// off/asleep or the receiver is unplugged.
|
||||
// Peripheral batteries: the Keychron mouse via the
|
||||
// mouse-battery script, the Nova Pro headset via
|
||||
// nova-anc. Polls only while the panel is up; each
|
||||
// row hides while its device is off/unplugged.
|
||||
Card {
|
||||
id: mouseBatCard
|
||||
visible: level > 0
|
||||
// Levels, not the rows' `visible`: that reads back
|
||||
// false while this card is hidden, so it'd latch.
|
||||
visible: mouseBat.level > 0 || headsetBat.level > 0
|
||||
width: parent.width
|
||||
|
||||
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(" ");
|
||||
mouseBatCard.level = parseInt(f[0]) || 0;
|
||||
mouseBatCard.charging = f[1] === "charging";
|
||||
mouseBat.level = parseInt(f[0]) || 0;
|
||||
mouseBat.charging = f[1] === "charging";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: headsetBatProc
|
||||
command: [Commands.novaAnc, "battery"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const f = text.trim().split(" ");
|
||||
headsetBat.level = parseInt(f[0]) || 0;
|
||||
headsetBat.charging = f[1] === "charging";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5508,49 +5578,24 @@ in
|
|||
repeat: true
|
||||
triggeredOnStart: true
|
||||
running: qsDropdown.visible
|
||||
onTriggered: mouseBatProc.running = true
|
||||
onTriggered: {
|
||||
mouseBatProc.running = true;
|
||||
headsetBatProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
BatteryRow {
|
||||
id: mouseBat
|
||||
width: parent.width
|
||||
height: 20
|
||||
icon: "mouse"
|
||||
label: "Mouse"
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "mouse"
|
||||
font.pixelSize: 15
|
||||
}
|
||||
SText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Mouse"
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: mouseBatCard.charging ? "battery-charging"
|
||||
: mouseBatCard.level >= 75 ? "battery-full"
|
||||
: mouseBatCard.level >= 50 ? "battery-medium"
|
||||
: mouseBatCard.level >= 20 ? "battery-low"
|
||||
: "battery"
|
||||
color: mouseBatCard.tint
|
||||
font.pixelSize: 15
|
||||
}
|
||||
SText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: mouseBatCard.level + "%"
|
||||
color: mouseBatCard.tint
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
BatteryRow {
|
||||
id: headsetBat
|
||||
width: parent.width
|
||||
icon: "headphones"
|
||||
label: "Headset"
|
||||
}
|
||||
}
|
||||
''}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue