Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-shell
# Conflicts: # shell/Ui/PillButton.qml # shell/plugins/bar/widgets/monitorPanel.qml # shell/plugins/bar/widgets/networkPanel.qml
This commit is contained in:
@@ -69,6 +69,19 @@ Item {
|
||||
property string tooltipText: ""
|
||||
property bool tooltipShown: false
|
||||
property var activePopout: null
|
||||
property var clickTargets: []
|
||||
|
||||
function registerClickTarget(target) {
|
||||
if (!target || clickTargets.indexOf(target) !== -1) return
|
||||
var next = clickTargets.slice()
|
||||
next.push(target)
|
||||
clickTargets = next
|
||||
}
|
||||
|
||||
function unregisterClickTarget(target) {
|
||||
var next = clickTargets.filter(function(item) { return item !== target })
|
||||
clickTargets = next
|
||||
}
|
||||
|
||||
function requestPopout(owner) {
|
||||
if (activePopout === owner) return
|
||||
@@ -267,6 +280,7 @@ Item {
|
||||
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false },
|
||||
"monitorPanel": { displayName: "Display", description: "Brightness slider and laptop display controls", category: "System", allowMultiple: false },
|
||||
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
|
||||
"powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false },
|
||||
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
|
||||
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
|
||||
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
|
||||
@@ -1086,6 +1100,14 @@ Item {
|
||||
signal pressed(int button)
|
||||
signal wheelMoved(int delta)
|
||||
|
||||
function triggerPress(button) {
|
||||
root.hideTooltip(buttonRoot)
|
||||
buttonRoot.pressed(button)
|
||||
}
|
||||
|
||||
Component.onCompleted: root.registerClickTarget(buttonRoot)
|
||||
Component.onDestruction: root.unregisterClickTarget(buttonRoot)
|
||||
|
||||
visible: text !== "" || keepSpace
|
||||
opacity: text === "" ? 0 : 1
|
||||
implicitWidth: fixedWidth > 0 ? fixedWidth : (root.vertical ? root.barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2 + rightExtraMargin))
|
||||
@@ -1113,7 +1135,7 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.showTooltip(buttonRoot, buttonRoot.tooltipText)
|
||||
onExited: root.hideTooltip(buttonRoot)
|
||||
onClicked: function(mouse) { buttonRoot.pressed(mouse.button) }
|
||||
onClicked: function(mouse) { buttonRoot.triggerPress(mouse.button) }
|
||||
onWheel: function(wheel) { buttonRoot.wheelMoved(wheel.angleDelta.y) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ Example `shell.json` (bar subtree only shown):
|
||||
],
|
||||
"right": [
|
||||
{ "id": "audioPanel" },
|
||||
{ "id": "battery" }
|
||||
{ "id": "powerPanel" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ Example `shell.json` (bar subtree only shown):
|
||||
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
|
||||
| `audioPanel` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = audio TUI · scroll = volume |
|
||||
| `networkPanel` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui |
|
||||
| `powerPanel` | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup |
|
||||
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
|
||||
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
|
||||
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
|
||||
|
||||
@@ -416,21 +416,12 @@ Item {
|
||||
|
||||
HeaderPill {
|
||||
pillIndex: 0
|
||||
property real scanRotation: 0
|
||||
iconText: ""
|
||||
iconRotation: root.adapter && root.adapter.discovering ? scanRotation : 0
|
||||
iconSpinning: root.adapter && root.adapter.discovering
|
||||
tooltipText: !root.adapter ? "" : !root.adapter.enabled ? "Bluetooth is off"
|
||||
: root.adapter.discovering ? "Stop scanning" : "Scan for devices"
|
||||
pillEnabled: root.adapter !== null && root.adapter.enabled
|
||||
onActivated: if (root.adapter) root.adapter.discovering = !root.adapter.discovering
|
||||
|
||||
NumberAnimation on scanRotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 900
|
||||
loops: Animation.Infinite
|
||||
running: root.adapter && root.adapter.discovering
|
||||
}
|
||||
}
|
||||
|
||||
HeaderPill {
|
||||
@@ -611,8 +602,8 @@ Item {
|
||||
if (dev.batteryAvailable) return "Connected · " + Math.round(dev.battery * 100) + "%"
|
||||
return "Connected"
|
||||
}
|
||||
if (isDiscovered) return "Available · click to pair"
|
||||
return "Paired"
|
||||
if (isDiscovered) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property color statusColor: {
|
||||
|
||||
@@ -45,7 +45,7 @@ Item {
|
||||
var list = []
|
||||
if (brightnessAvailable) list.push("brightness")
|
||||
list.push("scale")
|
||||
if (displays.length > 0) list.push("monitors")
|
||||
if (displays.length > 1) list.push("monitors")
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ Item {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
visible: root.displays.length > 0
|
||||
visible: root.displays.length > 1
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "Monitors"
|
||||
|
||||
@@ -78,9 +78,9 @@ Item {
|
||||
// — OnDemand only grants focus on click/hover.
|
||||
onPopupOpenChanged: {
|
||||
if (popupOpen) {
|
||||
refresh()
|
||||
refresh(true)
|
||||
selectedIndex = wifiNetworks.length > 0 ? 0 : -1
|
||||
focusSection = "dns"
|
||||
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
|
||||
var idx = dnsProviders.indexOf(dnsProvider)
|
||||
dnsIndex = idx >= 0 ? idx : 0
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
@@ -112,7 +112,7 @@ Item {
|
||||
function selectByDelta(delta) {
|
||||
if (wifiNetworks.length === 0) { selectedIndex = -1; return }
|
||||
if (selectedIndex < 0) selectedIndex = delta > 0 ? 0 : wifiNetworks.length - 1
|
||||
else selectedIndex = (selectedIndex + delta + wifiNetworks.length) % wifiNetworks.length
|
||||
else selectedIndex = Math.max(0, Math.min(wifiNetworks.length - 1, selectedIndex + delta))
|
||||
}
|
||||
|
||||
// Enter/Space on the highlighted row. Mirrors row-click semantics:
|
||||
@@ -165,6 +165,11 @@ iwctl known-networks list 2>/dev/null \\
|
||||
frequency = parts[3] || ""
|
||||
}
|
||||
|
||||
function copyToClipboard(value) {
|
||||
if (!value || !root.bar) return
|
||||
Quickshell.execDetached(["bash", "-lc", "printf %s " + root.bar.shellQuote(value) + " | wl-copy"])
|
||||
}
|
||||
|
||||
function networkTooltip() {
|
||||
if (kind === "wifi") {
|
||||
var f = parseFloat(frequency)
|
||||
@@ -216,7 +221,8 @@ iwctl known-networks list 2>/dev/null \\
|
||||
return ""
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
function refresh(scanWifi) {
|
||||
if (scanWifi === undefined) scanWifi = false
|
||||
if (!detailsProc.running) detailsProc.running = true
|
||||
if (!dnsProc.running) {
|
||||
dnsProc.command = ["bash", "-lc", root.dnsCommand("")]
|
||||
@@ -224,10 +230,38 @@ iwctl known-networks list 2>/dev/null \\
|
||||
}
|
||||
if (!wifiProc.running) {
|
||||
scanning = true
|
||||
wifiProc.command = ["bash", "-c", root.wifiScanScript(scanWifi)]
|
||||
wifiProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
function wifiScanScript(scanWifi) {
|
||||
return `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && exit 0
|
||||
echo STATION_AVAILABLE
|
||||
${scanWifi ? 'iwctl station "$station" scan >/dev/null 2>&1 || true' : ''}
|
||||
iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
| sed -e 's/\\x1b\\[[0-9;]*m//g' \\
|
||||
| awk '
|
||||
NR <= 4 { next }
|
||||
/^[[:space:]]*$/ { next }
|
||||
{
|
||||
connected = (substr($0, 1, 4) ~ />/) ? 1 : 0
|
||||
line = $0
|
||||
sub(/^[[:space:]]*>?[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
n = split(line, parts, /[[:space:]]{2,}/)
|
||||
if (n < 3) next
|
||||
ssid = parts[1]
|
||||
security = parts[n-1]
|
||||
dbm = parts[n] / 100
|
||||
signal = (dbm >= -50) ? 100 : (dbm <= -100) ? 0 : int(2 * (dbm + 100))
|
||||
printf "%d\\t%s\\t%d\\t%s\\n", connected, ssid, signal, security
|
||||
}'
|
||||
`
|
||||
}
|
||||
|
||||
function formatSpeed(mbps) {
|
||||
var v = parseInt(mbps, 10)
|
||||
if (!v || v < 0) return ""
|
||||
@@ -265,15 +299,17 @@ iwctl known-networks list 2>/dev/null \\
|
||||
// Format: connected<TAB>ssid<TAB>signal<TAB>security
|
||||
var parts = line.split("\t")
|
||||
if (parts.length < 3) continue
|
||||
var isConnected = parts[0] === "1"
|
||||
if (isConnected && parts[1] !== root.actionSsid) continue // Skip the connected network so it doesn't appear in the list, unless we are currently trying to connect to it
|
||||
|
||||
nets.push({
|
||||
connected: parts[0] === "1",
|
||||
connected: false,
|
||||
ssid: parts[1],
|
||||
signal: parseInt(parts[2], 10) || 0,
|
||||
security: parts[3] || ""
|
||||
})
|
||||
}
|
||||
nets.sort(function(a, b) {
|
||||
if (a.connected !== b.connected) return a.connected ? -1 : 1
|
||||
return b.signal - a.signal
|
||||
})
|
||||
wifiNetworks = nets
|
||||
@@ -347,8 +383,13 @@ iwctl --dont-ask station "$station" connect ${quotedSsid}
|
||||
var quotedPass = bar.shellQuote(passphrase)
|
||||
runAction("connect", ssid, `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && { echo "no Wi-Fi station available" >&2; exit 1; }
|
||||
iwctl --passphrase ${quotedPass} station "$station" connect ${quotedSsid}
|
||||
[[ -z $station ]] && { echo "No Wi-Fi station available" >&2; exit 1; }
|
||||
output=$(iwctl --passphrase ${quotedPass} station "$station" connect ${quotedSsid} 2>&1)
|
||||
exit_code=$?
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
echo "$output" | sed -e 's/\\x1b\\[[0-9;]*m//g' >&2
|
||||
exit $exit_code
|
||||
fi
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -455,29 +496,6 @@ fi
|
||||
// iwctl's table layout starts each row with one, which breaks naive parsing.
|
||||
Process {
|
||||
id: wifiProc
|
||||
command: ["bash", "-c", `
|
||||
station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }')
|
||||
[[ -z $station ]] && exit 0
|
||||
echo STATION_AVAILABLE
|
||||
iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
| sed -e 's/\\x1b\\[[0-9;]*m//g' \\
|
||||
| awk '
|
||||
NR <= 4 { next }
|
||||
/^[[:space:]]*$/ { next }
|
||||
{
|
||||
connected = (substr($0, 1, 4) ~ />/) ? 1 : 0
|
||||
line = $0
|
||||
sub(/^[[:space:]]*>?[[:space:]]+/, "", line)
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
n = split(line, parts, /[[:space:]]{2,}/)
|
||||
if (n < 3) next
|
||||
ssid = parts[1]
|
||||
security = parts[n-1]
|
||||
dbm = parts[n] / 100
|
||||
signal = (dbm >= -50) ? 100 : (dbm <= -100) ? 0 : int(2 * (dbm + 100))
|
||||
printf "%d\\t%s\\t%d\\t%s\\n", connected, ssid, signal, security
|
||||
}'
|
||||
`]
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.updateWifi(text)
|
||||
@@ -529,7 +547,14 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
else reason = "Failed to forget"
|
||||
}
|
||||
// Squash multi-line iwctl errors into a single readable line.
|
||||
root.failureReason = reason.split("\n").pop()
|
||||
// Also strip bash script error prefixes like "bash: line 4: " and "Operation failed"
|
||||
var finalReason = reason.split("\n").pop().replace(/^bash: line \d+: /, "").replace(/^Operation failed/, "").trim()
|
||||
if (!finalReason) {
|
||||
if (kind === "connect") finalReason = "Failed to connect"
|
||||
else if (kind === "disconnect") finalReason = "Failed to disconnect"
|
||||
else finalReason = "Failed to forget"
|
||||
}
|
||||
root.failureReason = finalReason
|
||||
}
|
||||
root.actionSsid = ""
|
||||
root.actionKind = ""
|
||||
@@ -652,159 +677,153 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
// Header — interface name + type, refresh on the right.
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(headerInfo.implicitHeight, refreshBtn.implicitHeight)
|
||||
height: Math.max(headerInfo.implicitHeight, headerActions.implicitHeight)
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: headerInfo
|
||||
anchors.left: parent.left
|
||||
anchors.right: headerActions.left
|
||||
anchors.rightMargin: Style.spacing.controlPaddingX
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(10)
|
||||
implicitHeight: Math.max(wifiToggleBtn.implicitHeight, wifiMainText.implicitHeight)
|
||||
|
||||
Text {
|
||||
text: root.icon
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
PanelActionButton {
|
||||
id: wifiToggleBtn
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: root.icon
|
||||
fontSize: Style.font.iconLarge
|
||||
size: Style.space(28)
|
||||
tooltipText: "Toggle Wi-Fi"
|
||||
foreground: root.bar.foreground
|
||||
hoverColor: root.bar.foreground // Override the dimming behavior
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
enabled: true
|
||||
onClicked: {
|
||||
root.bar.run("rfkill toggle wlan")
|
||||
Qt.callLater(function() { root.refresh(true) })
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Style.space(2)
|
||||
Row {
|
||||
anchors.left: wifiToggleBtn.right
|
||||
anchors.leftMargin: Style.spacing.controlPaddingX
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection")
|
||||
id: wifiMainText
|
||||
text: {
|
||||
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
|
||||
if (root.info.type === "ethernet") return "Ethernet"
|
||||
return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection")
|
||||
}
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, parent.width - (wifiFreqText.visible ? wifiFreqText.implicitWidth : 0))
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.info.type === "wifi") {
|
||||
var s = root.info.ssid || "Wi-Fi"
|
||||
if (root.info.freq) s += " · " + root.formatFreq(root.info.freq)
|
||||
return s
|
||||
}
|
||||
if (root.info.type === "ethernet") return "Ethernet"
|
||||
return ""
|
||||
}
|
||||
visible: text !== ""
|
||||
id: wifiFreqText
|
||||
visible: root.info.type === "wifi" && !!root.info.freq
|
||||
text: " • " + root.formatFreq(root.info.freq)
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: refreshBtn
|
||||
Row {
|
||||
id: headerActions
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: ""
|
||||
tooltipText: "Refresh"
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: Style.spacing.controlGap
|
||||
verticalPadding: Style.spacing.labelGap
|
||||
iconSize: Style.font.icon
|
||||
active: root.scanning
|
||||
onClicked: root.refresh()
|
||||
}
|
||||
}
|
||||
spacing: Style.spacing.controlGap
|
||||
|
||||
PanelSeparator {
|
||||
visible: !!root.info.iface
|
||||
foreground: root.bar.foreground
|
||||
PanelActionButton {
|
||||
id: disconnectBtn
|
||||
visible: root.info.type === "wifi" && !!root.info.ssid
|
||||
enabled: !root.busy
|
||||
iconText: ""
|
||||
tooltipText: "Disconnect"
|
||||
foreground: root.bar.foreground
|
||||
hoverColor: root.bar.urgent
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: root.disconnect(root.info.ssid)
|
||||
}
|
||||
|
||||
Button {
|
||||
id: refreshBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: ""
|
||||
iconSpinning: root.scanning
|
||||
tooltipText: "Refresh"
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: Style.spacing.controlGap
|
||||
verticalPadding: Style.spacing.labelGap
|
||||
iconSize: Style.font.icon
|
||||
active: root.scanning
|
||||
onClicked: root.refresh(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection details: IP, gateway, link speed, etc.
|
||||
Grid {
|
||||
Row {
|
||||
visible: !!root.info.iface
|
||||
width: parent.width
|
||||
columns: 2
|
||||
columnSpacing: Style.space(14)
|
||||
rowSpacing: Style.space(4)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(24)
|
||||
|
||||
// IP address.
|
||||
Text {
|
||||
visible: !!root.info.ip
|
||||
text: "IP address"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
visible: !!root.info.ip
|
||||
text: (root.info.ip || "") + (root.info.prefix ? "/" + root.info.prefix : "")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
Column {
|
||||
width: Style.space(140)
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair {
|
||||
visible: !!root.info.ip
|
||||
label: "IP"
|
||||
value: root.info.ip || ""
|
||||
copyable: true
|
||||
tooltipText: "Copy IP"
|
||||
}
|
||||
InfoPair {
|
||||
visible: !!root.info.gateway
|
||||
label: "Gateway"
|
||||
value: root.info.gateway || ""
|
||||
copyable: true
|
||||
tooltipText: "Copy gateway"
|
||||
}
|
||||
}
|
||||
|
||||
// Gateway.
|
||||
Text {
|
||||
visible: !!root.info.gateway
|
||||
text: "Gateway"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
visible: !!root.info.gateway
|
||||
text: root.info.gateway || ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Column {
|
||||
width: Style.space(140)
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
// Ethernet link speed / duplex.
|
||||
Text {
|
||||
visible: root.info.type === "ethernet" && !!root.info.speed
|
||||
text: "Link"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
visible: root.info.type === "ethernet" && !!root.info.speed
|
||||
text: root.formatSpeed(root.info.speed || "") + (root.info.duplex ? " · " + root.info.duplex + " duplex" : "")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
// Ethernet details
|
||||
InfoPair {
|
||||
visible: root.info.type === "ethernet" && !!root.info.speed
|
||||
label: "Link"
|
||||
value: root.formatSpeed(root.info.speed || "") + (root.info.duplex ? " • " + root.info.duplex + " dup" : "")
|
||||
}
|
||||
|
||||
// Wi-Fi signal.
|
||||
Text {
|
||||
visible: root.info.type === "wifi" && !!root.info.signal_dbm
|
||||
text: "Signal"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
visible: root.info.type === "wifi" && !!root.info.signal_dbm
|
||||
text: (root.info.signal_dbm || "") + " dBm"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
// Wi-Fi tx bitrate.
|
||||
Text {
|
||||
visible: root.info.type === "wifi" && !!root.info.bitrate
|
||||
text: "Link rate"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
Text {
|
||||
visible: root.info.type === "wifi" && !!root.info.bitrate
|
||||
text: root.info.bitrate || ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
// Wi-Fi details
|
||||
InfoPair {
|
||||
visible: root.info.type === "wifi" && !!root.info.signal_dbm
|
||||
label: "Signal"
|
||||
value: (root.info.signal_dbm || "") + " dBm"
|
||||
}
|
||||
InfoPair {
|
||||
visible: root.info.type === "wifi" && !!root.info.bitrate
|
||||
label: "Link"
|
||||
value: root.info.bitrate || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,10 +926,9 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One DNS provider pill. The cursor + current visuals come entirely from
|
||||
// CursorSurface; this component just binds them to the panel's cursor
|
||||
@@ -967,6 +985,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
|
||||
readonly property string statusText: {
|
||||
if (!net) return ""
|
||||
if (isPasswordOpen) return ""
|
||||
if (isBusy && root.actionKind === "connect") return "Connecting…"
|
||||
if (isBusy && root.actionKind === "disconnect") return "Disconnecting…"
|
||||
if (isBusy && root.actionKind === "forget") return "Forgetting…"
|
||||
@@ -1124,6 +1143,17 @@ iwctl known-networks list 2>/dev/null \\
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: failureTimer
|
||||
interval: 2000
|
||||
running: row.isFailed && row.isPasswordOpen
|
||||
onTriggered: {
|
||||
root.failureSsid = ""
|
||||
root.failureReason = ""
|
||||
pwField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Inline passphrase prompt — only shown when we hit a protected network
|
||||
// we don't have saved credentials for. Submitting (Enter or the check
|
||||
// button) fires connect; Esc cancels back to the row.
|
||||
@@ -1137,9 +1167,11 @@ iwctl known-networks list 2>/dev/null \\
|
||||
anchors.rightMargin: Style.space(10)
|
||||
anchors.topMargin: Style.space(4)
|
||||
implicitHeight: pwField.implicitHeight + Style.spacing.rowGap
|
||||
height: implicitHeight
|
||||
|
||||
TextField {
|
||||
id: pwField
|
||||
visible: !row.isBusy && !row.isFailed
|
||||
anchors.left: parent.left
|
||||
anchors.right: connectPwBtn.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -1162,14 +1194,38 @@ iwctl known-networks list 2>/dev/null \\
|
||||
Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: statusMsgWrapper
|
||||
visible: row.isBusy || row.isFailed
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: Style.spacing.controlHeight
|
||||
color: Style.normalFillFor(root.bar.foreground)
|
||||
border.color: Style.normalBorderFor(root.bar.foreground)
|
||||
border.width: Style.normalBorderWidth
|
||||
radius: Style.cornerRadius
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: row.isFailed ? "Wrong password" : "Connecting..."
|
||||
color: row.isFailed ? root.bar.urgent : root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
// 22×22 right-anchored to line up with forgetBtn and lockIndicator
|
||||
// above. Esc closes the prompt (handled by pwField.Keys.onEscapePressed)
|
||||
// so there's no separate cancel button.
|
||||
PanelActionButton {
|
||||
id: connectPwBtn
|
||||
visible: !row.isBusy && !row.isFailed
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: !root.busy && row.net && pwField.text.length > 0
|
||||
enabled: row.net && pwField.text.length > 0
|
||||
iconText: ""
|
||||
tooltipText: "Connect"
|
||||
foreground: root.bar.foreground
|
||||
@@ -1219,4 +1275,51 @@ iwctl known-networks list 2>/dev/null \\
|
||||
triggeredOnStart: true
|
||||
onTriggered: if (!networkProc.running) networkProc.running = true
|
||||
}
|
||||
|
||||
component InfoPair: Row {
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property bool copyable: false
|
||||
property string tooltipText: "Copy to clipboard"
|
||||
|
||||
width: parent.width
|
||||
spacing: Style.space(8)
|
||||
|
||||
InfoLabel { text: label }
|
||||
Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - valueText.implicitWidth - parent.spacing * 2); height: 1 }
|
||||
InfoValue {
|
||||
id: valueText
|
||||
text: value
|
||||
|
||||
MouseArea {
|
||||
id: valueMouse
|
||||
anchors.fill: parent
|
||||
enabled: copyable && valueText.text !== ""
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: root.copyToClipboard(valueText.text)
|
||||
}
|
||||
|
||||
PanelToolTip {
|
||||
visible: valueMouse.enabled && valueMouse.containsMouse
|
||||
text: tooltipText
|
||||
panelForeground: root.bar.foreground
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property QtObject bar: null
|
||||
property string moduleName: "powerPanel"
|
||||
property var settings: ({})
|
||||
|
||||
property bool popupOpen: false
|
||||
property var batteryInfo: ({})
|
||||
property var systemInfo: ({})
|
||||
property var profiles: []
|
||||
property string activeProfile: ""
|
||||
property int profileIndex: 0
|
||||
|
||||
function closePopout() { popupOpen = 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 (popupOpen && 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
|
||||
}
|
||||
|
||||
onPopupOpenChanged: {
|
||||
if (popupOpen) {
|
||||
refresh()
|
||||
var idx = profiles.indexOf(activeProfile)
|
||||
profileIndex = idx >= 0 ? idx : 0
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
IpcHandler {
|
||||
target: "powerPanel"
|
||||
function toggle(): void { root.popupOpen = !root.popupOpen }
|
||||
function show(): void { root.popupOpen = true }
|
||||
function hide(): void { root.closePopout() }
|
||||
}
|
||||
|
||||
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.popupOpen = !root.popupOpen }
|
||||
}
|
||||
|
||||
KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
contentWidth: panel.fittedContentWidth(Style.space(340))
|
||||
contentHeight: panel.fittedContentHeight(column.implicitHeight)
|
||||
|
||||
PanelKeyCatcher {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
onMoveRequested: function(dx, dy) {
|
||||
if (dx !== 0) root.selectProfileByDelta(dx)
|
||||
else if (dy !== 0) root.selectProfileByDelta(dy)
|
||||
}
|
||||
onActivateRequested: root.activateSelectedProfile()
|
||||
onCloseRequested: root.closePopout()
|
||||
|
||||
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 ? "Draw" : "Charge rate"; 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
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
horizontalPadding: Style.spacing.controlPaddingX
|
||||
verticalPadding: Style.spacing.controlPaddingY
|
||||
active: root.activeProfile === modelData
|
||||
hasCursor: root.profileIndex === index
|
||||
onClicked: root.setProfile(modelData)
|
||||
onHovered: function(h) {
|
||||
if (h) 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user