diff --git a/home/dot_config/quickshell/bar/Bar.qml b/home/dot_config/quickshell/bar/Bar.qml index 66bd5f1..7fbba41 100644 --- a/home/dot_config/quickshell/bar/Bar.qml +++ b/home/dot_config/quickshell/bar/Bar.qml @@ -31,6 +31,15 @@ PanelWindow { precision: SystemClock.Seconds } + DetailPopup { + id: detailsPopup + hardware: bar.hardware + sink: bar.sink + source: bar.source + battery: bar.battery + clock: clock + } + Row { id: leftModules anchors { @@ -137,7 +146,9 @@ PanelWindow { : Math.round(bar.source.audio.volume * 100) + "%") : "" onClicked: mouse => { - if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio) + if (mouse.button === Qt.LeftButton) + detailsPopup.toggle("audio", audioBlock); + else if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio) bar.sink.audio.muted = !bar.sink.audio.muted; else Quickshell.execDetached(["qjackctl"]); @@ -151,13 +162,18 @@ PanelWindow { } } - Network { service: bar.hardware } + Network { + service: bar.hardware + onDetailsRequested: anchorItem => detailsPopup.toggle("network", anchorItem) + } Backlight { service: bar.hardware } Block { + id: batteryBlock property int percentage: Math.round(bar.battery.percentage * 100) visible: bar.battery.ready && bar.battery.isLaptopBattery + interactive: true blockColor: percentage <= 15 && UPower.onBattery ? "#e91e63" : UPower.onBattery ? "#ffffff" : "#2980b9" textColor: UPower.onBattery && percentage > 15 @@ -171,6 +187,7 @@ PanelWindow { if (percentage <= 85) return "\uf241 " + percentage + "%"; return "\uf240 " + percentage + "%"; } + onClicked: detailsPopup.toggle("battery", batteryBlock) } Rectangle { @@ -233,12 +250,11 @@ PanelWindow { } Block { - property bool detailed: false + id: clockBlock interactive: true - text: Qt.formatDateTime(clock.date, - detailed ? "yyyy-MM-dd hh:mm:ss" : "hh:mm") - onClicked: detailed = !detailed + text: Qt.formatDateTime(clock.date, "hh:mm") + onClicked: detailsPopup.toggle("clock", clockBlock) } } } diff --git a/home/dot_config/quickshell/bar/DetailPopup.qml b/home/dot_config/quickshell/bar/DetailPopup.qml new file mode 100644 index 0000000..9f47ac7 --- /dev/null +++ b/home/dot_config/quickshell/bar/DetailPopup.qml @@ -0,0 +1,385 @@ +import QtQuick +import Quickshell +import Quickshell.Services.UPower + +PopupWindow { + id: root + + property var hardware + property var sink + property var source + property var battery + property var clock + property var anchorItem: null + property string page: "" + + function toggle(nextPage, item) { + if (visible && page === nextPage) { + visible = false; + return; + } + page = nextPage; + anchorItem = item; + if (page === "network") + hardware.refreshWifi(); + anchor.updateAnchor(); + visible = true; + } + + function duration(seconds) { + if (seconds <= 0) + return "Calculating"; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours + "h " + minutes + "m"; + } + + function batteryState() { + switch (battery.state) { + case UPowerDeviceState.Charging: return "Charging"; + case UPowerDeviceState.Discharging: return "Discharging"; + case UPowerDeviceState.FullyCharged: return "Fully charged"; + case UPowerDeviceState.Empty: return "Empty"; + case UPowerDeviceState.PendingCharge: return "Waiting to charge"; + case UPowerDeviceState.PendingDischarge: return "Waiting to discharge"; + default: return "Unknown"; + } + } + + visible: false + implicitWidth: 320 + implicitHeight: contentLoader.item ? contentLoader.item.implicitHeight + 32 : 100 + color: "transparent" + + anchor { + item: root.anchorItem + edges: Edges.Bottom | Edges.Right + gravity: Edges.Bottom | Edges.Left + margins.top: 4 + } + + Rectangle { + anchors.fill: parent + color: "#e0000000" + + Loader { + id: contentLoader + anchors { + left: parent.left + right: parent.right + top: parent.top + margins: 12 + } + sourceComponent: root.page === "clock" ? clockPage + : root.page === "audio" ? audioPage + : root.page === "network" ? networkPage + : batteryPage + } + + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 3 + color: "#b3e5fc" + } + } + + Component { + id: clockPage + + Item { + id: clockContent + property date now: root.clock.date + property int year: now.getFullYear() + property int month: now.getMonth() + property int firstDay: new Date(year, month, 1).getDay() + property int days: new Date(year, month + 1, 0).getDate() + + implicitWidth: 288 + implicitHeight: 310 + + PopupHeader { + width: parent.width + text: Qt.formatDateTime(parent.now, "dddd, MMMM d") + } + + Text { + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 40 + } + text: Qt.formatDateTime(parent.now, "hh:mm:ss") + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 30 + } + + Row { + anchors { + left: parent.left + top: parent.top + topMargin: 86 + } + spacing: 4 + + Repeater { + model: ["S", "M", "T", "W", "T", "F", "S"] + Text { + required property string modelData + width: 37 + text: modelData + color: "#7aa6da" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + horizontalAlignment: Text.AlignHCenter + font.bold: true + } + } + } + + Grid { + anchors { + left: parent.left + top: parent.top + topMargin: 112 + } + columns: 7 + spacing: 4 + + Repeater { + model: 42 + + Rectangle { + required property int index + property int day: index - clockContent.firstDay + 1 + property bool today: day === clockContent.now.getDate() + + width: 37 + height: 29 + radius: 0 + color: today ? "#0288d1" : "transparent" + + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 3 + visible: parent.today + color: "#b3e5fc" + } + + Text { + anchors.centerIn: parent + text: parent.day > 0 && parent.day <= clockContent.days + ? parent.day : "" + color: parent.today ? "#ffffff" : "#cfd8dc" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 14 + } + } + } + } + } + } + + Component { + id: audioPage + + Column { + width: 288 + spacing: 12 + + PopupHeader { + width: parent.width + text: "Audio" + } + + PopupSlider { + width: parent.width + label: "Output" + value: root.sink && root.sink.audio ? root.sink.audio.volume : 0 + muted: root.sink && root.sink.audio ? root.sink.audio.muted : false + onValueRequested: value => { + if (root.sink && root.sink.audio) + root.sink.audio.volume = value; + } + onMuteRequested: { + if (root.sink && root.sink.audio) + root.sink.audio.muted = !root.sink.audio.muted; + } + } + + PopupSlider { + width: parent.width + label: "Microphone" + value: root.source && root.source.audio ? root.source.audio.volume : 0 + muted: root.source && root.source.audio ? root.source.audio.muted : false + onValueRequested: value => { + if (root.source && root.source.audio) + root.source.audio.volume = value; + } + onMuteRequested: { + if (root.source && root.source.audio) + root.source.audio.muted = !root.source.audio.muted; + } + } + } + } + + Component { + id: networkPage + + Column { + width: 288 + spacing: 10 + + PopupHeader { + width: parent.width + text: "Network" + } + + Text { + text: "Connected to " + root.hardware.connection + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 15 + } + + Column { + id: networks + width: parent.width + spacing: 5 + + Repeater { + model: root.hardware.wifiNetworks + + Rectangle { + required property var modelData + width: networks.width + height: 29 + radius: 0 + color: modelData.active ? "#0288d1" : "#80000000" + + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 3 + visible: parent.modelData.active + color: "#b3e5fc" + } + + Text { + anchors { + left: parent.left + leftMargin: 9 + verticalCenter: parent.verticalCenter + } + width: parent.width - 70 + elide: Text.ElideRight + text: modelData.ssid + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 14 + } + + Text { + anchors { + right: parent.right + rightMargin: 9 + verticalCenter: parent.verticalCenter + } + text: modelData.signal + "%" + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 13 + } + } + } + } + + Rectangle { + width: parent.width + height: 30 + radius: 0 + color: "#0288d1" + + Text { + anchors.centerIn: parent + text: "Open network settings" + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 13 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Quickshell.execDetached(["nm-connection-editor"]) + } + } + } + } + + Component { + id: batteryPage + + Column { + width: 288 + spacing: 12 + + PopupHeader { + width: parent.width + text: "Battery" + } + + Text { + text: Math.round(root.battery.percentage * 100) + "% " + + root.batteryState() + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 25 + } + + Rectangle { + width: parent.width + height: 8 + radius: 0 + color: "#40000000" + + Rectangle { + width: parent.width * root.battery.percentage + height: parent.height + radius: 0 + color: root.battery.percentage <= 0.15 ? "#e91e63" : "#0288d1" + } + } + + Text { + text: UPower.onBattery + ? "Remaining: " + root.duration(root.battery.timeToEmpty) + : "Until full: " + root.duration(root.battery.timeToFull) + color: "#b3e5fc" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 15 + } + + Text { + text: "Rate: " + Math.abs(root.battery.changeRate).toFixed(1) + " W" + + (root.battery.healthSupported + ? " Health: " + Math.round(root.battery.healthPercentage * 100) + "%" + : "") + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 14 + } + } + } +} diff --git a/home/dot_config/quickshell/bar/HardwareService.qml b/home/dot_config/quickshell/bar/HardwareService.qml index aa2479d..d52e81e 100644 --- a/home/dot_config/quickshell/bar/HardwareService.qml +++ b/home/dot_config/quickshell/bar/HardwareService.qml @@ -7,12 +7,18 @@ Scope { property string connection: "Disconnected" property int brightness: -1 + property var wifiNetworks: [] function setBrightness(change) { Quickshell.execDetached(["brightnessctl", "set", change]); brightnessRefresh.restart(); } + function refreshWifi() { + if (!wifiQuery.running) + wifiQuery.running = true; + } + Process { id: networkQuery command: ["nmcli", "-t", "-f", "TYPE,NAME", "connection", "show", "--active"] @@ -76,4 +82,35 @@ Scope { interval: 250 onTriggered: brightnessQuery.running = true } + + Process { + id: wifiQuery + command: ["nmcli", "-t", "-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list", "--rescan", "auto"] + + stdout: StdioCollector { + onStreamFinished: { + const networks = []; + const seen = {}; + for (let line of text.trim().split("\n")) { + const fields = line.split(":"); + if (fields.length < 3) + continue; + const active = fields.shift() === "*"; + const signal = parseInt(fields.pop()); + const ssid = fields.join(":").replace(/\\:/g, ":"); + if (!ssid || seen[ssid]) + continue; + seen[ssid] = true; + networks.push({ + "ssid": ssid, + "signal": signal, + "active": active + }); + if (networks.length >= 8) + break; + } + root.wifiNetworks = networks; + } + } + } } diff --git a/home/dot_config/quickshell/bar/Network.qml b/home/dot_config/quickshell/bar/Network.qml index ff66be8..0487ff9 100644 --- a/home/dot_config/quickshell/bar/Network.qml +++ b/home/dot_config/quickshell/bar/Network.qml @@ -6,9 +6,15 @@ Block { required property var service property bool connected: service.connection !== "Disconnected" + signal detailsRequested(var anchorItem) blockColor: connected ? "#2980b9" : "#e91e63" interactive: true text: connected ? "\uf1eb " + service.connection : "Disconnected !" - onClicked: Quickshell.execDetached(["nm-connection-editor"]) + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) + detailsRequested(root); + else + Quickshell.execDetached(["nm-connection-editor"]); + } } diff --git a/home/dot_config/quickshell/bar/PopupHeader.qml b/home/dot_config/quickshell/bar/PopupHeader.qml new file mode 100644 index 0000000..e948c90 --- /dev/null +++ b/home/dot_config/quickshell/bar/PopupHeader.qml @@ -0,0 +1,23 @@ +import QtQuick + +Rectangle { + id: root + + property string text: "" + + implicitHeight: 32 + color: "#0288d1" + + Text { + anchors { + left: parent.left + leftMargin: 10 + verticalCenter: parent.verticalCenter + } + text: root.text + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 18 + font.bold: true + } +} diff --git a/home/dot_config/quickshell/bar/PopupSlider.qml b/home/dot_config/quickshell/bar/PopupSlider.qml new file mode 100644 index 0000000..5793d1a --- /dev/null +++ b/home/dot_config/quickshell/bar/PopupSlider.qml @@ -0,0 +1,82 @@ +import QtQuick + +Item { + id: root + + property string label: "" + property real value: 0 + property bool muted: false + signal valueRequested(real value) + signal muteRequested() + + implicitWidth: 280 + implicitHeight: 58 + + Text { + anchors.left: parent.left + text: root.label + color: "#ffffff" + font.family: "SF Pro Display, Helvetica, Arial, sans-serif" + font.pixelSize: 15 + } + + Rectangle { + anchors { + right: parent.right + top: parent.top + } + width: 58 + height: 24 + radius: 0 + color: root.muted ? "#e91e63" : "#0288d1" + + Text { + anchors.centerIn: parent + text: root.muted ? "MUTED" : Math.round(root.value * 100) + "%" + color: "#ffffff" + font.pixelSize: 11 + font.bold: true + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.muteRequested() + } + } + + Rectangle { + id: track + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 8 + radius: 0 + color: "#40000000" + + Rectangle { + width: Math.min(parent.width, parent.width * root.value / 1.5) + height: parent.height + radius: 0 + color: root.muted ? "#e91e63" : "#0288d1" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + function updateValue(mouse) { + root.valueRequested(Math.max(0, + Math.min(1.5, mouse.x / width * 1.5))); + } + + onPressed: mouse => updateValue(mouse) + onPositionChanged: mouse => { + if (pressed) + updateValue(mouse); + } + } + } +}