mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-04 12:47:51 +02:00
315 lines
10 KiB
QML
315 lines
10 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.UPower
|
|
import qs.Commons
|
|
import qs.Ui
|
|
|
|
Panel {
|
|
id: root
|
|
moduleName: "powerPanel"
|
|
ipcTarget: "panels.power"
|
|
property var batteryInfo: ({})
|
|
property var systemInfo: ({})
|
|
property var profiles: []
|
|
property string activeProfile: ""
|
|
property int profileIndex: 0
|
|
property bool cursorActive: false
|
|
|
|
function selectProfileByDelta(delta) {
|
|
if (profiles.length === 0) { profileIndex = 0; return }
|
|
profileIndex = Math.max(0, Math.min(profiles.length - 1, profileIndex + delta))
|
|
}
|
|
|
|
function activateSelectedProfile() {
|
|
if (profileIndex < 0 || profileIndex >= profiles.length) return
|
|
setProfile(profiles[profileIndex])
|
|
}
|
|
|
|
function batteryIcon() {
|
|
var device = UPower.displayDevice
|
|
if (!device || !device.isPresent) return ""
|
|
|
|
var chargingIcons = ["", "", "", "", "", "", "", "", "", ""]
|
|
var defaultIcons = ["", "", "", "", "", "", "", "", "", ""]
|
|
var index = Math.max(0, Math.min(9, Math.floor(device.percentage * 10)))
|
|
|
|
if (device.state === UPowerDeviceState.FullyCharged) return ""
|
|
if (!UPower.onBattery && device.state !== UPowerDeviceState.Charging) return ""
|
|
if (device.state === UPowerDeviceState.Charging) return chargingIcons[index]
|
|
return defaultIcons[index]
|
|
}
|
|
|
|
function modeLabel() {
|
|
var device = UPower.displayDevice
|
|
var percentage = device && device.isPresent ? device.percentage : 0
|
|
|
|
if (!UPower.onBattery && percentage >= 1) {
|
|
return "Fully charged"
|
|
} else if (UPower.onBattery) {
|
|
return "Battery"
|
|
} else {
|
|
return "Charging"
|
|
}
|
|
}
|
|
|
|
readonly property bool fullyCharged: {
|
|
var device = UPower.displayDevice
|
|
return device && device.isPresent && device.state === UPowerDeviceState.FullyCharged
|
|
}
|
|
|
|
function refresh() {
|
|
if (!batteryProc.running) batteryProc.running = true
|
|
if (!profilesProc.running) profilesProc.running = true
|
|
if (!systemProc.running) systemProc.running = true
|
|
}
|
|
|
|
function updateKeyValue(raw, targetName) {
|
|
var next = {}
|
|
var lines = String(raw || "").split("\n")
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var idx = lines[i].indexOf("\t")
|
|
if (idx <= 0) continue
|
|
next[lines[i].substring(0, idx)] = lines[i].substring(idx + 1).trim()
|
|
}
|
|
if (targetName === "battery") batteryInfo = next
|
|
else systemInfo = next
|
|
}
|
|
|
|
function updateProfiles(raw) {
|
|
var lines = String(raw || "").split("\n")
|
|
var list = []
|
|
var active = ""
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i].trim()
|
|
if (!line) continue
|
|
var parts = line.split("\t")
|
|
list.push(parts[0])
|
|
if (parts[1] === "1") active = parts[0]
|
|
}
|
|
profiles = list
|
|
activeProfile = active
|
|
if (profileIndex >= profiles.length) profileIndex = Math.max(0, profiles.length - 1)
|
|
if (opened && activeProfile !== "") {
|
|
var idx = profiles.indexOf(activeProfile)
|
|
if (idx >= 0) profileIndex = idx
|
|
}
|
|
}
|
|
|
|
function setProfile(profile) {
|
|
if (!profile || actionProc.running) return
|
|
actionProc.command = ["powerprofilesctl", "set", profile]
|
|
actionProc.running = true
|
|
}
|
|
|
|
onOpenedChanged: {
|
|
if (opened) {
|
|
refresh()
|
|
var idx = profiles.indexOf(activeProfile)
|
|
profileIndex = idx >= 0 ? idx : 0
|
|
cursorActive = false
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: refresh()
|
|
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
|
|
Process {
|
|
id: batteryProc
|
|
command: ["bash", "-lc", `
|
|
bat=$(upower -e 2>/dev/null | grep BAT | head -n1)
|
|
[[ -z $bat ]] && exit 0
|
|
info=$(upower -i "$bat")
|
|
printf 'percentage\t%s\n' "$(awk '/percentage/ { print int($2) "%"; exit }' <<<"$info")"
|
|
printf 'state\t%s\n' "$(awk '/state/ { print $2; exit }' <<<"$info")"
|
|
printf 'rate\t%s\n' "$(awk '/energy-rate/ { v=sprintf("%.1f", $2); sub(/\.0$/, "", v); print v "W"; exit }' <<<"$info")"
|
|
printf 'size\t%s\n' "$(awk '/energy-full:/ { printf "%dWh", $2; exit }' <<<"$info")"
|
|
printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/null)"
|
|
`]
|
|
stdout: StdioCollector { waitForEnd: true; onStreamFinished: root.updateKeyValue(text, "battery") }
|
|
}
|
|
|
|
Process {
|
|
id: profilesProc
|
|
command: ["bash", "-lc", "powerprofilesctl list 2>/dev/null | awk '/^\\s*[* ]\\s*[a-zA-Z0-9-]+:$/ { active=($1==\"*\"); gsub(/^[*[:space:]]+|:$/,\"\"); print $0 \"\\t\" (active ? 1 : 0) }'"]
|
|
stdout: StdioCollector { waitForEnd: true; onStreamFinished: root.updateProfiles(text) }
|
|
}
|
|
|
|
Process {
|
|
id: systemProc
|
|
command: ["bash", "-lc", "cpu=$(top -bn1 | awk '/^%?Cpu/ { gsub(/,/, \"\"); for (i=1; i<=NF; i++) if ($(i+1) == \"id\") { printf \"%.0f%%\", 100 - $i; exit } }'); awk -v cpu=\"$cpu\" '/^MemTotal:/ { total=$2 } /^MemAvailable:/ { avail=$2 } END { used=total-avail; printf \"cpu\\t%s\\n\", cpu; printf \"memory\\t%.1fGB / %.0fGB\\n\", used/1024/1024, total/1024/1024 }' /proc/meminfo"]
|
|
stdout: StdioCollector { waitForEnd: true; onStreamFinished: root.updateKeyValue(text, "system") }
|
|
}
|
|
|
|
Process {
|
|
id: actionProc
|
|
onExited: root.refresh()
|
|
}
|
|
|
|
Timer { interval: 5000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.refresh() }
|
|
|
|
WidgetButton {
|
|
id: button
|
|
anchors.fill: parent
|
|
bar: root.bar
|
|
text: root.batteryIcon()
|
|
horizontalMargin: 8.5
|
|
rightExtraMargin: 2
|
|
active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery
|
|
tooltipText: ""
|
|
onPressed: function(b) { root.toggle() }
|
|
}
|
|
|
|
KeyboardPanel {
|
|
id: panel
|
|
anchorItem: button
|
|
owner: root
|
|
bar: root.bar
|
|
open: root.opened
|
|
focusTarget: keyCatcher
|
|
contentWidth: panel.fittedContentWidth(Style.space(340))
|
|
contentHeight: panel.fittedContentHeight(column.implicitHeight)
|
|
|
|
PanelKeyCatcher {
|
|
id: keyCatcher
|
|
anchors.fill: parent
|
|
onMoveRequested: function(dx, dy) {
|
|
if (!root.cursorActive) { root.cursorActive = true; return }
|
|
if (dx !== 0) root.selectProfileByDelta(dx)
|
|
else if (dy !== 0) root.selectProfileByDelta(dy)
|
|
}
|
|
onActivateRequested: if (root.cursorActive) root.activateSelectedProfile()
|
|
onCloseRequested: root.close()
|
|
|
|
Column {
|
|
id: column
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
spacing: Style.space(16)
|
|
|
|
Item {
|
|
width: parent.width
|
|
implicitHeight: Style.space(28)
|
|
|
|
Item {
|
|
id: iconWrapper
|
|
width: Style.space(28)
|
|
height: Style.space(28)
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
Text {
|
|
id: acIcon
|
|
text: root.batteryIcon()
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.iconLarge
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: root.modeLabel()
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.subtitle
|
|
font.bold: true
|
|
anchors.left: iconWrapper.right
|
|
anchors.leftMargin: Style.spacing.controlPaddingX
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
Row {
|
|
visible: root.batteryInfo.percentage !== undefined && !root.fullyCharged
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Style.space(24)
|
|
|
|
Column {
|
|
width: Style.space(140)
|
|
spacing: Style.spacing.labelGap
|
|
InfoPair { label: "Percentage"; value: root.batteryInfo.percentage || "" }
|
|
InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" }
|
|
}
|
|
|
|
Column {
|
|
width: Style.space(140)
|
|
spacing: Style.spacing.labelGap
|
|
InfoPair { label: UPower.onBattery ? "Time left" : "Time to full"; value: root.batteryInfo.time || "—" }
|
|
InfoPair { label: UPower.onBattery ? "Discharging" : "Charging"; value: root.batteryInfo.rate || "" }
|
|
}
|
|
}
|
|
|
|
PanelSeparator {
|
|
visible: !root.fullyCharged
|
|
foreground: root.bar.foreground
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Style.space(12)
|
|
PanelSectionHeader {
|
|
visible: !root.fullyCharged
|
|
text: "POWER PROFILE"
|
|
foreground: root.bar.foreground
|
|
fontFamily: root.bar.fontFamily
|
|
}
|
|
Row {
|
|
width: parent.width
|
|
spacing: Style.space(6)
|
|
Repeater {
|
|
model: root.profiles
|
|
Button {
|
|
required property var modelData
|
|
required property int index
|
|
text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1)
|
|
foreground: root.bar.foreground
|
|
fontFamily: root.bar.fontFamily
|
|
horizontalPadding: Style.spacing.controlPaddingX
|
|
verticalPadding: Style.spacing.controlPaddingY
|
|
active: root.activeProfile === modelData
|
|
hasCursor: root.cursorActive && root.profileIndex === index
|
|
onClicked: root.setProfile(modelData)
|
|
onHovered: function(h) {
|
|
if (h) {
|
|
root.cursorActive = true
|
|
root.profileIndex = index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component InfoPair: Row {
|
|
property string label: ""
|
|
property string value: ""
|
|
|
|
width: parent.width
|
|
spacing: Style.space(8)
|
|
|
|
InfoLabel { text: label }
|
|
Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth - parent.spacing * 2); height: 1 }
|
|
InfoValue { text: value }
|
|
}
|
|
|
|
component InfoLabel: Text {
|
|
color: root.bar.foreground
|
|
opacity: 0.6
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
component InfoValue: Text {
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
}
|