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
|
# packet; opcode 0xbd sets ANC (0 off / 1 transparent / 2 on), 0xb0
|
||||||
# requests full status (anc @ 0x0a, power @ 0x0f, 0x08 = on-head).
|
# requests full status (anc @ 0x0a, power @ 0x0f, 0x08 = on-head).
|
||||||
# Protocol lifted from Arctis-Sound-Manager's nova_pro_wireless.yaml.
|
# 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" ''
|
novaAncScript = pkgs.writeScript "nova-anc" ''
|
||||||
#!${pkgs.python3}/bin/python3
|
#!${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
|
import glob, os, select, sys, time
|
||||||
|
|
||||||
MODES = {"off": 0, "transparent": 1, "on": 2}
|
MODES = {"off": 0, "transparent": 1, "on": 2}
|
||||||
|
|
@ -261,7 +264,8 @@ in
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
fd = os.open(dev, os.O_RDWR)
|
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"))
|
os.write(fd, bytes([0x06, 0xbd, MODES[sys.argv[1]]]).ljust(64, b"\0"))
|
||||||
print(sys.argv[1])
|
print(sys.argv[1])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
@ -274,6 +278,12 @@ in
|
||||||
pkt = os.read(fd, 64)
|
pkt = os.read(fd, 64)
|
||||||
# Skip unsolicited 0x07xx event packets until the status reply
|
# Skip unsolicited 0x07xx event packets until the status reply
|
||||||
if len(pkt) > 0x0f and pkt[0] == 0x06 and pkt[1] == 0xb0:
|
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:
|
if pkt[0x0f] != 0x08:
|
||||||
break # headset off / in cradle -> report nothing
|
break # headset off / in cradle -> report nothing
|
||||||
print(NAMES.get(pkt[0x0a], "off"))
|
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
|
// ── PillSlider: rounded track + fill + knob with a full-height
|
||||||
// invisible hit area. `value` is 0..1; `moved(v)` fires on drag.
|
// invisible hit area. `value` is 0..1; `moved(v)` fires on drag.
|
||||||
// Knob diameter derives from the track, so the small app-stream
|
// Knob diameter derives from the track, so the small app-stream
|
||||||
|
|
@ -5475,30 +5539,36 @@ in
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
${lib.optionalString (!isMacbook) ''
|
${lib.optionalString (!isMacbook) ''
|
||||||
// Wireless mouse battery, read off the Keychron
|
// Peripheral batteries: the Keychron mouse via the
|
||||||
// receiver by the mouse-battery script. Polls only
|
// mouse-battery script, the Nova Pro headset via
|
||||||
// while the panel is up; hidden while the mouse is
|
// nova-anc. Polls only while the panel is up; each
|
||||||
// off/asleep or the receiver is unplugged.
|
// row hides while its device is off/unplugged.
|
||||||
Card {
|
Card {
|
||||||
id: mouseBatCard
|
// Levels, not the rows' `visible`: that reads back
|
||||||
visible: level > 0
|
// false while this card is hidden, so it'd latch.
|
||||||
|
visible: mouseBat.level > 0 || headsetBat.level > 0
|
||||||
width: parent.width
|
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 {
|
Process {
|
||||||
id: mouseBatProc
|
id: mouseBatProc
|
||||||
command: [Commands.mouseBattery]
|
command: [Commands.mouseBattery]
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const f = text.trim().split(" ");
|
const f = text.trim().split(" ");
|
||||||
mouseBatCard.level = parseInt(f[0]) || 0;
|
mouseBat.level = parseInt(f[0]) || 0;
|
||||||
mouseBatCard.charging = f[1] === "charging";
|
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
|
repeat: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
running: qsDropdown.visible
|
running: qsDropdown.visible
|
||||||
onTriggered: mouseBatProc.running = true
|
onTriggered: {
|
||||||
|
mouseBatProc.running = true;
|
||||||
|
headsetBatProc.running = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
BatteryRow {
|
||||||
|
id: mouseBat
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 20
|
icon: "mouse"
|
||||||
|
label: "Mouse"
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
BatteryRow {
|
||||||
spacing: 6
|
id: headsetBat
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
width: parent.width
|
||||||
SIcon {
|
icon: "headphones"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
label: "Headset"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue