From c517997922d7ed7917e9cd64cc6a41c49759145b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 20:47:30 +0200 Subject: [PATCH 01/10] Spinning when scanning --- shell/Ui/PillButton.qml | 12 +++- shell/plugins/bar/widgets/bluetoothPanel.qml | 11 +--- shell/plugins/bar/widgets/networkPanel.qml | 59 +++++++++++--------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/shell/Ui/PillButton.qml b/shell/Ui/PillButton.qml index cf5bdc49..34b81f08 100644 --- a/shell/Ui/PillButton.qml +++ b/shell/Ui/PillButton.qml @@ -18,6 +18,7 @@ Rectangle { property real fontSize: 12 property real iconSize: 14 property real iconRotation: 0 + property bool iconSpinning: false property real horizontalPadding: 10 property real verticalPadding: 6 property bool active: false @@ -112,14 +113,23 @@ Rectangle { spacing: 8 Text { + id: iconLabel visible: root.iconText !== "" text: root.iconText color: root.foreground font.family: root.fontFamily font.pixelSize: root.iconSize - rotation: root.iconRotation + rotation: root.iconSpinning ? 0 : root.iconRotation transformOrigin: Item.Center anchors.verticalCenter: parent.verticalCenter + + RotationAnimation on rotation { + from: 0 + to: 360 + duration: 900 + loops: Animation.Infinite + running: root.iconSpinning + } } Text { diff --git a/shell/plugins/bar/widgets/bluetoothPanel.qml b/shell/plugins/bar/widgets/bluetoothPanel.qml index 51986fae..a5e10c6c 100644 --- a/shell/plugins/bar/widgets/bluetoothPanel.qml +++ b/shell/plugins/bar/widgets/bluetoothPanel.qml @@ -415,21 +415,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 { diff --git a/shell/plugins/bar/widgets/networkPanel.qml b/shell/plugins/bar/widgets/networkPanel.qml index 3c3a9762..6617526f 100644 --- a/shell/plugins/bar/widgets/networkPanel.qml +++ b/shell/plugins/bar/widgets/networkPanel.qml @@ -77,7 +77,7 @@ Item { // — OnDemand only grants focus on click/hover. onPopupOpenChanged: { if (popupOpen) { - refresh() + refresh(true) selectedIndex = wifiNetworks.length > 0 ? 0 : -1 focusSection = "dns" var idx = dnsProviders.indexOf(dnsProvider) @@ -215,7 +215,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("")] @@ -223,10 +224,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 "" @@ -454,29 +483,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) @@ -701,6 +707,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\ anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter iconText: "󰑐" + iconSpinning: root.scanning tooltipText: "Refresh" tooltipBackground: root.bar.background tooltipForeground: root.bar.foreground @@ -709,7 +716,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\ verticalPadding: 4 iconSize: 14 active: root.scanning - onClicked: root.refresh() + onClicked: root.refresh(true) } } From 1dc2f1ec79c195f65288cdb371864e91439bd487 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 20:47:37 +0200 Subject: [PATCH 02/10] Introduce power panel --- shell/plugins/bar/Bar.qml | 1 + shell/plugins/bar/README.md | 3 +- shell/plugins/bar/widgets/powerPanel.qml | 214 +++++++++++++++++++++++ shell/shell-defaults.json | 2 +- 4 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 shell/plugins/bar/widgets/powerPanel.qml diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index c6ce3630..4b85fc52 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -266,6 +266,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 }, diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 3bc72c1d..74906b6c 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -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 | diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml new file mode 100644 index 00000000..aa4a7128 --- /dev/null +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -0,0 +1,214 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.UPower +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: "" + + function closePopout() { popupOpen = false } + + 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() { return UPower.onBattery ? "Battery" : "AC" } + + 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 + } + + function setProfile(profile) { + if (!profile || actionProc.running) return + actionProc.command = ["powerprofilesctl", "set", profile] + actionProc.running = true + } + + 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 + active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery + tooltipText: "" + onPressed: function(b) { root.popupOpen = !root.popupOpen; if (root.popupOpen) root.refresh() } + } + + KeyboardPanel { + id: panel + anchorItem: button + owner: root + bar: root.bar + open: root.popupOpen + contentWidth: 340 + contentHeight: column.implicitHeight + 28 + + Column { + id: column + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 12 + + Row { + spacing: 10 + Text { text: root.batteryIcon(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 22 } + Text { text: root.modeLabel(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 13; font.bold: true } + } + + Grid { + visible: root.batteryInfo.percentage !== undefined + width: parent.width + columns: 2 + columnSpacing: 14 + rowSpacing: 4 + InfoLabel { text: "Percentage" } InfoValue { text: root.batteryInfo.percentage || "" } + InfoLabel { text: UPower.onBattery ? "Time left" : "Time to full" } InfoValue { text: root.batteryInfo.time || "—" } + InfoLabel { text: "Battery size" } InfoValue { text: root.batteryInfo.size || "" } + InfoLabel { text: UPower.onBattery ? "Draw" : "Charge rate" } InfoValue { text: root.batteryInfo.rate || "" } + } + + PanelSeparator { foreground: root.bar.foreground } + PanelSectionHeader { text: "Power profile"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } + Row { + width: parent.width + spacing: 6 + Repeater { + model: root.profiles + CursorPill { + required property var modelData + 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: 10 + verticalPadding: 6 + active: root.activeProfile === modelData + onClicked: root.setProfile(modelData) + } + } + } + + PanelSeparator { foreground: root.bar.foreground } + PanelSectionHeader { text: "System"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } + Grid { + width: parent.width + columns: 2 + columnSpacing: 14 + rowSpacing: 4 + InfoLabel { text: "CPU" } InfoValue { text: root.systemInfo.cpu || "—" } + InfoLabel { text: "Memory" } InfoValue { text: root.systemInfo.memory || "—" } + } + } + } + + component InfoLabel: Text { + color: Qt.darker(root.bar.foreground, 1.4) + font.family: root.bar.fontFamily + font.pixelSize: 11 + } + + component InfoValue: Text { + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 11 + } +} diff --git a/shell/shell-defaults.json b/shell/shell-defaults.json index f3280488..c64f6f34 100644 --- a/shell/shell-defaults.json +++ b/shell/shell-defaults.json @@ -56,7 +56,7 @@ "id": "monitorPanel" }, { - "id": "battery" + "id": "powerPanel" } ] } From 196dcd2472ecf9e2b7c27eca173b1d343f57d1b8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 21:13:37 +0200 Subject: [PATCH 03/10] Open power panel on super + ctrl + p --- default/hypr/bindings/utilities.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index 46477064..24adf821 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -57,6 +57,7 @@ hl.bind("SUPER + CTRL + A", hl.dsp.exec_cmd("omarchy-shell audioPanel toggle"), hl.bind("SUPER + CTRL + B", hl.dsp.exec_cmd("omarchy-shell bluetoothPanel toggle"), { description = "Bluetooth panel" }) hl.bind("SUPER + CTRL + D", hl.dsp.exec_cmd("omarchy-shell monitorPanel toggle"), { description = "Display panel" }) hl.bind("SUPER + CTRL + W", hl.dsp.exec_cmd("omarchy-shell networkPanel toggle"), { description = "Network panel" }) +hl.bind("SUPER + CTRL + P", hl.dsp.exec_cmd("omarchy-shell powerPanel toggle"), { description = "Power panel" }) hl.bind("SUPER + CTRL + T", hl.dsp.exec_cmd("omarchy-launch-tui btop"), { description = "Activity" }) hl.bind("SUPER + CTRL + X", hl.dsp.exec_cmd("voxtype record toggle"), { description = "Toggle dictation" }) From d99ccaf5501f055befdfde7b380b36434d08348c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 21:13:42 +0200 Subject: [PATCH 04/10] More power panel layouting --- shell/plugins/bar/widgets/powerPanel.qml | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index aa4a7128..16b5adae 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -153,16 +153,24 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n Text { text: root.modeLabel(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 13; font.bold: true } } - Grid { + Row { visible: root.batteryInfo.percentage !== undefined width: parent.width - columns: 2 - columnSpacing: 14 - rowSpacing: 4 - InfoLabel { text: "Percentage" } InfoValue { text: root.batteryInfo.percentage || "" } - InfoLabel { text: UPower.onBattery ? "Time left" : "Time to full" } InfoValue { text: root.batteryInfo.time || "—" } - InfoLabel { text: "Battery size" } InfoValue { text: root.batteryInfo.size || "" } - InfoLabel { text: UPower.onBattery ? "Draw" : "Charge rate" } InfoValue { text: root.batteryInfo.rate || "" } + spacing: 20 + + Column { + width: (parent.width - parent.spacing) / 2 + spacing: 4 + InfoPair { label: "Percentage"; value: root.batteryInfo.percentage || "" } + InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" } + } + + Column { + width: (parent.width - parent.spacing) / 2 + spacing: 4 + 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 { foreground: root.bar.foreground } @@ -200,6 +208,18 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } } + component InfoPair: Row { + property string label: "" + property string value: "" + + width: parent.width + spacing: 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: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily From e8bfdbb13b9bdd3698a345f595c6bf5e0495b20b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 21:24:48 +0200 Subject: [PATCH 05/10] Little more tweaks --- shell/plugins/bar/widgets/powerPanel.qml | 75 ++++++++++++++---------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index 16b5adae..be2ce764 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -145,28 +145,29 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: 12 + spacing: 16 Row { spacing: 10 - Text { text: root.batteryIcon(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 22 } - Text { text: root.modeLabel(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 13; font.bold: true } + anchors.left: parent.left + Text { id: acIcon; text: root.batteryIcon(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 22 } + Text { text: root.modeLabel(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 13; font.bold: true; anchors.verticalCenter: acIcon.verticalCenter } } Row { visible: root.batteryInfo.percentage !== undefined - width: parent.width - spacing: 20 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 24 Column { - width: (parent.width - parent.spacing) / 2 + width: 140 spacing: 4 InfoPair { label: "Percentage"; value: root.batteryInfo.percentage || "" } InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" } } Column { - width: (parent.width - parent.spacing) / 2 + width: 140 spacing: 4 InfoPair { label: UPower.onBattery ? "Time left" : "Time to full"; value: root.batteryInfo.time || "—" } InfoPair { label: UPower.onBattery ? "Draw" : "Charge rate"; value: root.batteryInfo.rate || "" } @@ -174,36 +175,47 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } PanelSeparator { foreground: root.bar.foreground } - PanelSectionHeader { text: "Power profile"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } - Row { + + Column { width: parent.width - spacing: 6 - Repeater { - model: root.profiles - CursorPill { - required property var modelData - 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: 10 - verticalPadding: 6 - active: root.activeProfile === modelData - onClicked: root.setProfile(modelData) + spacing: 12 + PanelSectionHeader { text: "POWER PROFILE"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } + Row { + width: parent.width + spacing: 6 + Repeater { + model: root.profiles + CursorPill { + required property var modelData + text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1) + foreground: root.bar.foreground + background: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04) + tooltipBackground: root.bar.background + tooltipForeground: root.bar.foreground + fontFamily: root.bar.fontFamily + horizontalPadding: 10 + verticalPadding: 6 + active: root.activeProfile === modelData + onClicked: root.setProfile(modelData) + } } } } PanelSeparator { foreground: root.bar.foreground } - PanelSectionHeader { text: "System"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } - Grid { + + Column { width: parent.width - columns: 2 - columnSpacing: 14 - rowSpacing: 4 - InfoLabel { text: "CPU" } InfoValue { text: root.systemInfo.cpu || "—" } - InfoLabel { text: "Memory" } InfoValue { text: root.systemInfo.memory || "—" } + spacing: 12 + PanelSectionHeader { text: "SYSTEM"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } + Grid { + width: parent.width + columns: 2 + columnSpacing: 14 + rowSpacing: 4 + InfoLabel { text: "CPU" } InfoValue { text: root.systemInfo.cpu || "—" } + InfoLabel { text: "Mem" } InfoValue { text: root.systemInfo.memory || "—" } + } } } } @@ -221,7 +233,8 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } component InfoLabel: Text { - color: Qt.darker(root.bar.foreground, 1.4) + color: root.bar.foreground + opacity: 0.6 font.family: root.bar.fontFamily font.pixelSize: 11 } From 74f71236e074ccdf77c1d07f110afb154bb797d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 22:00:09 +0200 Subject: [PATCH 06/10] More tweaking --- shell/Ui/PanelActionButton.qml | 2 +- shell/plugins/bar/widgets/networkPanel.qml | 322 ++++++++++++--------- shell/plugins/bar/widgets/powerPanel.qml | 55 ++-- 3 files changed, 227 insertions(+), 152 deletions(-) diff --git a/shell/Ui/PanelActionButton.qml b/shell/Ui/PanelActionButton.qml index 333beed1..00facf17 100644 --- a/shell/Ui/PanelActionButton.qml +++ b/shell/Ui/PanelActionButton.qml @@ -68,7 +68,7 @@ Rectangle { anchors.centerIn: parent text: root.iconText color: root.enabled - ? (root._hot ? root.hoverColor : Qt.darker(root.foreground, 1.3)) + ? (root._hot ? root.hoverColor : root.foreground) : Qt.darker(root.foreground, 2.0) font.family: root.fontFamily font.pixelSize: root.fontSize diff --git a/shell/plugins/bar/widgets/networkPanel.qml b/shell/plugins/bar/widgets/networkPanel.qml index 6617526f..2f49a59f 100644 --- a/shell/plugins/bar/widgets/networkPanel.qml +++ b/shell/plugins/bar/widgets/networkPanel.qml @@ -79,7 +79,7 @@ Item { if (popupOpen) { 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() }) @@ -111,7 +111,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: @@ -293,15 +293,17 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\ // Format: connectedssidsignalsecurity 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 @@ -375,8 +377,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 `) } @@ -534,7 +541,14 @@ fi 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 = "" @@ -657,160 +671,148 @@ fi // 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: 10 anchors.verticalCenter: parent.verticalCenter - spacing: 10 + implicitHeight: Math.max(wifiToggleBtn.implicitHeight, wifiMainText.implicitHeight) - Text { - text: root.icon - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 22 + PanelActionButton { + id: wifiToggleBtn + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + iconText: root.icon + fontSize: 22 + size: 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: 2 + Row { + anchors.left: wifiToggleBtn.right + anchors.leftMargin: 10 + 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: 13 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: 10 + font.pixelSize: 13 + font.bold: true } } } - PillButton { - id: refreshBtn + Row { + id: headerActions anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - iconText: "󰑐" - iconSpinning: root.scanning - tooltipText: "Refresh" - tooltipBackground: root.bar.background - tooltipForeground: root.bar.foreground - foreground: root.bar.foreground - horizontalPadding: 8 - verticalPadding: 4 - iconSize: 14 - active: root.scanning - onClicked: root.refresh(true) - } - } + spacing: 8 - PanelSeparator { - visible: !!root.info.iface - foreground: root.bar.foreground + PanelActionButton { + id: disconnectBtn + visible: root.info.type === "wifi" && !!root.info.ssid + 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) + } + + PillButton { + 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: 8 + verticalPadding: 4 + iconSize: 14 + 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: 14 - rowSpacing: 4 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 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: 11 - } - 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: 11 + Column { + width: 140 + spacing: 4 + InfoPair { + visible: !!root.info.ip + label: "IP" + value: root.info.ip || "" + } + InfoPair { + visible: !!root.info.gateway + label: "Gateway" + value: root.info.gateway || "" + } } - // Gateway. - Text { - visible: !!root.info.gateway - text: "Gateway" - color: Qt.darker(root.bar.foreground, 1.4) - font.family: root.bar.fontFamily - font.pixelSize: 11 - } - Text { - visible: !!root.info.gateway - text: root.info.gateway || "" - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 11 - } + Column { + width: 140 + spacing: 4 - // 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: 11 - } - 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: 11 - } + // 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: 11 - } - 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: 11 - } - - // 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: 11 - } - Text { - visible: root.info.type === "wifi" && !!root.info.bitrate - text: root.info.bitrate || "" - color: root.bar.foreground - font.family: root.bar.fontFamily - font.pixelSize: 11 + // 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 || "" + } } } @@ -973,6 +975,7 @@ fi 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…" @@ -1130,6 +1133,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. @@ -1142,10 +1156,11 @@ iwctl known-networks list 2>/dev/null \\ anchors.leftMargin: 10 anchors.rightMargin: 10 anchors.topMargin: 4 - implicitHeight: pwField.implicitHeight + 8 + implicitHeight: 34 TextField { id: pwField + visible: !row.isBusy && !row.isFailed anchors.left: parent.left anchors.right: connectPwBtn.left anchors.verticalCenter: parent.verticalCenter @@ -1157,7 +1172,7 @@ iwctl known-networks list 2>/dev/null \\ foreground: root.bar.foreground horizontalPadding: 8 verticalPadding: 6 - enabled: !row.isBusy + implicitHeight: 26 onAccepted: { if (!root.busy && row.net && text.length > 0) root.connectWithPassphrase(row.net.ssid, text) @@ -1168,14 +1183,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: 26 + color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04) + border.color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) + border.width: 1 + radius: 4 + + Text { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: row.isFailed ? (root.failureReason || "Wrong password") : "Connecting..." + color: row.isFailed ? root.bar.urgent : root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 12 + } + } + // 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 @@ -1225,4 +1264,29 @@ 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: "" + + width: parent.width + spacing: 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: 11 + } + + component InfoValue: Text { + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 11 + } } diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index be2ce764..ac42a3ba 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -33,7 +33,7 @@ Item { return defaultIcons[index] } - function modeLabel() { return UPower.onBattery ? "Battery" : "AC" } + function modeLabel() { return UPower.onBattery ? "Battery" : "Charging" } function refresh() { if (!batteryProc.running) batteryProc.running = true @@ -126,6 +126,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n 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; if (root.popupOpen) root.refresh() } @@ -147,11 +148,37 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n anchors.top: parent.top spacing: 16 - Row { - spacing: 10 - anchors.left: parent.left - Text { id: acIcon; text: root.batteryIcon(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 22 } - Text { text: root.modeLabel(); color: root.bar.foreground; font.family: root.bar.fontFamily; font.pixelSize: 13; font.bold: true; anchors.verticalCenter: acIcon.verticalCenter } + Item { + width: parent.width + implicitHeight: 28 + + Item { + id: iconWrapper + width: 28 + height: 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: 22 + anchors.centerIn: parent + } + } + + Text { + text: root.modeLabel() + color: root.bar.foreground + font.family: root.bar.fontFamily + font.pixelSize: 13 + font.bold: true + anchors.left: iconWrapper.right + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + } } Row { @@ -201,22 +228,6 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } } } - - PanelSeparator { foreground: root.bar.foreground } - - Column { - width: parent.width - spacing: 12 - PanelSectionHeader { text: "SYSTEM"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } - Grid { - width: parent.width - columns: 2 - columnSpacing: 14 - rowSpacing: 4 - InfoLabel { text: "CPU" } InfoValue { text: root.systemInfo.cpu || "—" } - InfoLabel { text: "Mem" } InfoValue { text: root.systemInfo.memory || "—" } - } - } } } From 9daa0e6ed39e68e7cca6e4683cbdd9525c6f9576 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 22:10:26 +0200 Subject: [PATCH 07/10] More tweaks --- shell/plugins/bar/widgets/monitorPanel.qml | 4 +-- shell/plugins/bar/widgets/networkPanel.qml | 40 +++++++++++++++++++--- shell/plugins/bar/widgets/powerPanel.qml | 13 ++++++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/shell/plugins/bar/widgets/monitorPanel.qml b/shell/plugins/bar/widgets/monitorPanel.qml index 851dad98..101bb273 100644 --- a/shell/plugins/bar/widgets/monitorPanel.qml +++ b/shell/plugins/bar/widgets/monitorPanel.qml @@ -44,7 +44,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 } @@ -530,7 +530,7 @@ Item { Column { width: parent.width spacing: 6 - visible: root.displays.length > 0 + visible: root.displays.length > 1 PanelSectionHeader { text: "Monitors" diff --git a/shell/plugins/bar/widgets/networkPanel.qml b/shell/plugins/bar/widgets/networkPanel.qml index 2f49a59f..ed5cfe02 100644 --- a/shell/plugins/bar/widgets/networkPanel.qml +++ b/shell/plugins/bar/widgets/networkPanel.qml @@ -164,6 +164,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) @@ -783,11 +788,15 @@ fi 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" } } @@ -1157,6 +1166,7 @@ iwctl known-networks list 2>/dev/null \\ anchors.rightMargin: 10 anchors.topMargin: 4 implicitHeight: 34 + height: implicitHeight TextField { id: pwField @@ -1165,6 +1175,7 @@ iwctl known-networks list 2>/dev/null \\ anchors.right: connectPwBtn.left anchors.verticalCenter: parent.verticalCenter anchors.rightMargin: 6 + height: 26 password: true placeholderText: "Passphrase" font.family: root.bar.fontFamily @@ -1172,7 +1183,6 @@ iwctl known-networks list 2>/dev/null \\ foreground: root.bar.foreground horizontalPadding: 8 verticalPadding: 6 - implicitHeight: 26 onAccepted: { if (!root.busy && row.net && text.length > 0) root.connectWithPassphrase(row.net.ssid, text) @@ -1199,7 +1209,7 @@ iwctl known-networks list 2>/dev/null \\ anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: row.isFailed ? (root.failureReason || "Wrong password") : "Connecting..." + text: row.isFailed ? "Wrong password" : "Connecting..." color: row.isFailed ? root.bar.urgent : root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 12 @@ -1268,13 +1278,35 @@ iwctl known-networks list 2>/dev/null \\ component InfoPair: Row { property string label: "" property string value: "" + property bool copyable: false + property string tooltipText: "Copy to clipboard" width: parent.width spacing: 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 } + 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 { diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index ac42a3ba..0f4bbbae 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -33,7 +33,18 @@ Item { return defaultIcons[index] } - function modeLabel() { return UPower.onBattery ? "Battery" : "Charging" } + 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" + } + } function refresh() { if (!batteryProc.running) batteryProc.running = true From 1a457118a575b5e7c9261f10b9e08d16523d3e6b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 22:38:53 +0200 Subject: [PATCH 08/10] Ensure you can click to open another panel while one is open --- shell/Ui/KeyboardPanel.qml | 64 +++++++++++++++++++++++++++++++------- shell/Ui/WidgetButton.qml | 21 ++++++++++--- shell/plugins/bar/Bar.qml | 23 +++++++++++++- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/shell/Ui/KeyboardPanel.qml b/shell/Ui/KeyboardPanel.qml index a3228351..63932fc7 100644 --- a/shell/Ui/KeyboardPanel.qml +++ b/shell/Ui/KeyboardPanel.qml @@ -83,20 +83,17 @@ PanelWindow { right: true } - // Clickable region = whole screen MINUS the bar's strip. Clicks on the - // bar pass through to the bar layer; clicks anywhere else are caught - // by us and either land on the card (no-op) or trigger dismissal. - readonly property real _barStripSize: bar ? bar.barSize : 0 + // Clickable region is the whole screen. Clicks in the bar strip are + // forwarded to registered bar buttons so switching between panel icons + // works in one click even when the overlay surface is above the bar. + readonly property real _barStripSize: { + if (!bar) return 0 + var actual = (root.barPos === "top" || root.barPos === "bottom") ? root.barH : root.barW + return Math.max(bar.barSize, actual) + root.gap + } mask: Region { width: root.screenW height: root.screenH - Region { - x: root.barPos === "right" ? root.screenW - root._barStripSize : 0 - y: root.barPos === "bottom" ? root.screenH - root._barStripSize : 0 - width: (root.barPos === "top" || root.barPos === "bottom") ? root.screenW : root._barStripSize - height: (root.barPos === "top" || root.barPos === "bottom") ? root._barStripSize : root.screenH - intersection: Intersection.Subtract - } } // Track every layout change between the bar's contentItem and the @@ -177,9 +174,52 @@ PanelWindow { // during the fade-out so the dying overlay doesn't swallow clicks that // were meant for the apps behind it. MouseArea { + id: dismissArea anchors.fill: parent enabled: root.open - onClicked: root.closePopout() + hoverEnabled: true + property bool hoveringBar: false + cursorShape: hoveringBar ? Qt.PointingHandCursor : Qt.ArrowCursor + + function inBarRegion(px, py) { + if (root.barPos === "bottom") return py >= root.screenH - root._barStripSize + if (root.barPos === "left") return px <= root._barStripSize + if (root.barPos === "right") return px >= root.screenW - root._barStripSize + return py <= root._barStripSize + } + + function barPoint(px, py) { + if (root.barPos === "bottom") return Qt.point(px, py - (root.screenH - root.barH)) + if (root.barPos === "right") return Qt.point(px - (root.screenW - root.barW), py) + return Qt.point(px, py) + } + + function pressTargetAt(px, py) { + if (!root.anchorWindow || !root.anchorWindow.contentItem || !root.bar || !root.bar.clickTargets) return null + var p = barPoint(px, py) + var targets = root.bar.clickTargets + for (var i = targets.length - 1; i >= 0; i--) { + var target = targets[i] + if (!target || !target.triggerPress || target.visible === false || target.opacity === 0 || !target.mapToItem) continue + var pos = root.anchorWindow.itemPosition(target) + if (p.x >= pos.x && p.x <= pos.x + target.width && p.y >= pos.y && p.y <= pos.y + target.height) return target + } + return null + } + + function forwardBarClick(px, py, button) { + var target = pressTargetAt(px, py) + if (!target) return false + target.triggerPress(button) + return true + } + + onPositionChanged: function(mouse) { hoveringBar = inBarRegion(mouse.x, mouse.y) } + onExited: hoveringBar = false + onClicked: function(mouse) { + if (inBarRegion(mouse.x, mouse.y) && forwardBarClick(mouse.x, mouse.y, mouse.button)) return + root.closePopout() + } } // --- card ---------------------------------------------------------------- diff --git a/shell/Ui/WidgetButton.qml b/shell/Ui/WidgetButton.qml index 8f59b8d6..991f3bd1 100644 --- a/shell/Ui/WidgetButton.qml +++ b/shell/Ui/WidgetButton.qml @@ -18,10 +18,26 @@ Item { property real textRotation: 0 property bool keepSpace: false property string tooltipText: "" + property var registeredBar: null signal pressed(int button) signal wheelMoved(int delta) + function triggerPress(button) { + if (root.bar) root.bar.hideTooltip(root) + root.pressed(button) + } + + function syncClickRegistration() { + if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(root) + registeredBar = root.bar + if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(root) + } + + onBarChanged: syncClickRegistration() + Component.onCompleted: syncClickRegistration() + Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(root) + readonly property bool vertical: bar ? bar.vertical : false readonly property int barSize: bar ? bar.barSize : 26 @@ -60,10 +76,7 @@ Item { cursorShape: Qt.PointingHandCursor onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText) onExited: if (root.bar) root.bar.hideTooltip(root) - onClicked: function(mouse) { - if (root.bar) root.bar.hideTooltip(root) - root.pressed(mouse.button) - } + onClicked: function(mouse) { root.triggerPress(mouse.button) } onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) } } } diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 4b85fc52..13f81b25 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -68,6 +68,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 @@ -1086,6 +1099,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 +1134,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) } } } From bc1b82489110a01a32b2f8badc65853f9a4d207d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 22:47:06 +0200 Subject: [PATCH 09/10] More tweaks --- shell/plugins/bar/widgets/bluetoothPanel.qml | 4 ++-- shell/plugins/bar/widgets/powerPanel.qml | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/shell/plugins/bar/widgets/bluetoothPanel.qml b/shell/plugins/bar/widgets/bluetoothPanel.qml index a5e10c6c..d276cdc6 100644 --- a/shell/plugins/bar/widgets/bluetoothPanel.qml +++ b/shell/plugins/bar/widgets/bluetoothPanel.qml @@ -601,8 +601,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: { diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index 0f4bbbae..c28a7287 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -46,6 +46,11 @@ Item { } } + 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 @@ -193,7 +198,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } Row { - visible: root.batteryInfo.percentage !== undefined + visible: root.batteryInfo.percentage !== undefined && !root.fullyCharged anchors.horizontalCenter: parent.horizontalCenter spacing: 24 @@ -212,12 +217,20 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n } } - PanelSeparator { foreground: root.bar.foreground } + PanelSeparator { + visible: !root.fullyCharged + foreground: root.bar.foreground + } Column { width: parent.width spacing: 12 - PanelSectionHeader { text: "POWER PROFILE"; foreground: root.bar.foreground; fontFamily: root.bar.fontFamily } + PanelSectionHeader { + visible: !root.fullyCharged + text: "POWER PROFILE" + foreground: root.bar.foreground + fontFamily: root.bar.fontFamily + } Row { width: parent.width spacing: 6 From 34bb9508303ca056b6774d5460c7e4a1d5432984 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 18 May 2026 22:50:38 +0200 Subject: [PATCH 10/10] Keyboard nav --- shell/plugins/bar/widgets/powerPanel.qml | 55 +++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/shell/plugins/bar/widgets/powerPanel.qml b/shell/plugins/bar/widgets/powerPanel.qml index c28a7287..fc1e7bce 100644 --- a/shell/plugins/bar/widgets/powerPanel.qml +++ b/shell/plugins/bar/widgets/powerPanel.qml @@ -16,9 +16,20 @@ Item { 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 "" @@ -82,6 +93,11 @@ Item { } 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) { @@ -90,6 +106,15 @@ Item { 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 @@ -145,7 +170,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n rightExtraMargin: 2 active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery tooltipText: "" - onPressed: function(b) { root.popupOpen = !root.popupOpen; if (root.popupOpen) root.refresh() } + onPressed: function(b) { root.popupOpen = !root.popupOpen } } KeyboardPanel { @@ -157,12 +182,22 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n contentWidth: 340 contentHeight: column.implicitHeight + 28 - Column { - id: column - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: 16 + 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: 16 Item { width: parent.width @@ -238,6 +273,7 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n model: root.profiles CursorPill { required property var modelData + required property int index text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1) foreground: root.bar.foreground background: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04) @@ -247,13 +283,18 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n horizontalPadding: 10 verticalPadding: 6 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: ""