From 20909d32a1241b5e074802f4735b0c69595375c4 Mon Sep 17 00:00:00 2001 From: Dillon Johnson Date: Thu, 21 Aug 2025 21:16:23 -1000 Subject: [PATCH 01/54] add power menu ipc handler --- Modules/IPC/IPCManager.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 8554fff..f4988d5 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -73,4 +73,12 @@ Item { BrightnessService.decreaseBrightness() } } + + IpcHandler { + target: "powerMenu" + + function toggle() { + powerPanel.toggle(Quickshell.screens[0]) + } + } } From 5d5e1f52dde9212adfa75c23984a3d34a7f969a4 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 09:22:20 -0700 Subject: [PATCH 02/54] fix accuracy of comment in mediamini --- Modules/Bar/MediaMini.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/MediaMini.qml index a061031..b332667 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/MediaMini.qml @@ -145,7 +145,7 @@ Row { NText { id: titleText - // If hovered or just switched window, show up to 300 pixels + // If hovered or just switched window, show up to 400 pixels // If not hovered show up to 150 pixels width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, 400 * scaling) : Math.min(fullTitleMetrics.contentWidth, From 22ed13bd56d110dd5a41474228d59b4bc042fc2e Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 09:26:10 -0700 Subject: [PATCH 03/54] match miniplayer & activewindow max widths --- Modules/Bar/ActiveWindow.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/ActiveWindow.qml index ec7b42b..a788b50 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/ActiveWindow.qml @@ -100,10 +100,10 @@ Row { NText { id: titleText - // If hovered or just switched window, show up to 300 pixels + // If hovered or just switched window, show up to 400 pixels // If not hovered show up to 150 pixels width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth, - 300 * scaling) : Math.min( + 400 * scaling) : Math.min( fullTitleMetrics.contentWidth, 150 * scaling) text: getTitle() font.pointSize: Style.fontSizeS * scaling From b0774f4ea42f3f3dbbe8b2c13b4ee87bcbe4ee93 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 09:46:29 -0700 Subject: [PATCH 04/54] fix: run flatpak gpu-screen-recorder if applicable --- Services/ScreenRecorderService.qml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 03560e7..28e08a7 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -30,8 +30,18 @@ Singleton { videoDir += "/" } outputPath = videoDir + filename - var command = `gpu-screen-recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}` - + var command = ` + _gpuscreenrecorder_flatpak_installed() { + flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder" + } + if command -v gpu-screen-recorder >/dev/null 2>&1; then + gpu-screen-recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath} + elif command -v flatpak >/dev/null 2>&1 && _gpuscreenrecorder_flatpak_installed; then + flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath} + else + notify-send "gpu-screen-recorder not installed!" -u critical + fi`; + //Logger.log("ScreenRecorder", command) Quickshell.execDetached(["sh", "-c", command]) Logger.log("ScreenRecorder", "Started recording") From c64f6dd430168870d76a7178adcb0873abd4e938 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 09:50:21 -0700 Subject: [PATCH 05/54] make flags a variable --- Services/ScreenRecorderService.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 28e08a7..57f66d9 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -30,14 +30,15 @@ Singleton { videoDir += "/" } outputPath = videoDir + filename + var flags = `-w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}` var command = ` _gpuscreenrecorder_flatpak_installed() { flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder" } if command -v gpu-screen-recorder >/dev/null 2>&1; then - gpu-screen-recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath} + gpu-screen-recorder ${flags} elif command -v flatpak >/dev/null 2>&1 && _gpuscreenrecorder_flatpak_installed; then - flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath} + flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder ${flags} else notify-send "gpu-screen-recorder not installed!" -u critical fi`; From 97455a862ec20c39de3054742df3356c9ebbdc9d Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 10:11:51 -0700 Subject: [PATCH 06/54] feat: make audio visualizer FPS configurable --- Commons/Settings.qml | 1 + Modules/SettingsPanel/Tabs/AudioTab.qml | 33 +++++++++++++++++++++++++ Services/CavaService.qml | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 23fa432..08cebaf 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -234,6 +234,7 @@ Singleton { property bool showMiniplayerCava: false property string visualizerType: "linear" property int volumeStep: 5 + property int cavaFps: 60 } // ui diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index a644a47..9f992da 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -308,6 +308,39 @@ ColumnLayout { Settings.data.audio.visualizerType = key } } + + NText { + text: "Audio Visualizer FPS" + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + RowLayout { + NSlider { + Layout.fillWidth: true + from: 0 + to: 360 + value: Settings.data.audio.cavaFps + stepSize: 1 + onMoved: { + Settings.data.audio.cavaFps = value + } + } + + NText { + text: Settings.data.audio.cavaFps + " FPS" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + color: Color.mOnSurface + } + } + } } } } diff --git a/Services/CavaService.qml b/Services/CavaService.qml index 4d41a91..c8d6f05 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -14,7 +14,7 @@ Singleton { property var config: ({ "general": { "bars": barsCount, - "framerate": 60, + "framerate": Settings.data.audio.cavaFps, "autosens": 1, "sensitivity": 100, "lower_cutoff_freq": 50, From 32e6828cc4c694aad5854d4bec6f372a666a6833 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 11:03:55 -0700 Subject: [PATCH 07/54] use dropdown for framerate selector, update variable name, add common FPSes (144 & 165) to FPS dropdowns --- Commons/Settings.qml | 2 +- Modules/SettingsPanel/Tabs/AudioTab.qml | 58 ++++++++++--------- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 8 +++ Services/CavaService.qml | 2 +- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 08cebaf..df6692f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -234,7 +234,7 @@ Singleton { property bool showMiniplayerCava: false property string visualizerType: "linear" property int volumeStep: 5 - property int cavaFps: 60 + property int cavaFrameRate: 60 } // ui diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 9f992da..2e57df5 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -309,37 +309,39 @@ ColumnLayout { } } - NText { - text: "Audio Visualizer FPS" - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - - ColumnLayout { - spacing: Style.marginS * scaling - Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling - - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 360 - value: Settings.data.audio.cavaFps - stepSize: 1 - onMoved: { - Settings.data.audio.cavaFps = value - } + NComboBox { + label: "Frame Rate" + description: "Target frame rate for audio visualizer. (default: 60)" + model: ListModel { + ListElement { + key: "30" + name: "30 FPS" } - - NText { - text: Settings.data.audio.cavaFps + " FPS" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface + ListElement { + key: "60" + name: "60 FPS" + } + ListElement { + key: "120" + name: "120 FPS" + } + ListElement { + key: "144" + name: "144 FPS" + } + ListElement { + key: "165" + name: "165 FPS" + } + ListElement { + key: "240" + name: "240 FPS" } } + currentKey: Settings.data.audio.cavaFrameRate + onSelected: key => { + Settings.data.audio.cavaFrameRate = key + } } } } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index a02b3a3..6d23795 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -121,6 +121,14 @@ ColumnLayout { key: "120" name: "120 FPS" } + ListElement { + key: "144" + name: "144 FPS" + } + ListElement { + key: "165" + name: "165 FPS" + } ListElement { key: "240" name: "240 FPS" diff --git a/Services/CavaService.qml b/Services/CavaService.qml index c8d6f05..cb7b603 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -14,7 +14,7 @@ Singleton { property var config: ({ "general": { "bars": barsCount, - "framerate": Settings.data.audio.cavaFps, + "framerate": Settings.data.audio.cavaFrameRate, "autosens": 1, "sensitivity": 100, "lower_cutoff_freq": 50, From 1d88ec9018d76adcb58ceaa7d121fbbee031ad00 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 11:06:57 -0700 Subject: [PATCH 08/54] add 100 FPS as common target --- Modules/SettingsPanel/Tabs/AudioTab.qml | 4 ++++ Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 2e57df5..2f64aca 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -321,6 +321,10 @@ ColumnLayout { key: "60" name: "60 FPS" } + ListElement { + key: "100" + name: "100 FPS" + } ListElement { key: "120" name: "120 FPS" diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 6d23795..39b8b35 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -117,6 +117,10 @@ ColumnLayout { key: "60" name: "60 FPS" } + ListElement { + key: "100" + name: "100 FPS" + } ListElement { key: "120" name: "120 FPS" From cd102d894e2538ebdf265e23e10cc4f60dba418b Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 12:34:08 -0700 Subject: [PATCH 09/54] feat: show ethernet icon if ethernet is connected Closes https://github.com/noctalia-dev/noctalia-shell/issues/115 --- Modules/Bar/WiFi.qml | 3 ++- Services/NetworkService.qml | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml index 6b9b921..da749ff 100644 --- a/Modules/Bar/WiFi.qml +++ b/Modules/Bar/WiFi.qml @@ -21,6 +21,7 @@ NIconButton { colorBorderHover: Color.transparent icon: { + if (NetworkService.ethernet) return "lan" let connected = false let signalStrength = 0 for (const net in NetworkService.networks) { @@ -30,7 +31,7 @@ NIconButton { break } } - return connected ? NetworkService.signalIcon(signalStrength) : "wifi" + return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" } tooltipText: "WiFi Networks" onClicked: { diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 65378f0..43e31e7 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -16,6 +16,7 @@ Singleton { property string detectedInterface: "" property string lastConnectedNetwork: "" property bool isLoading: false + property bool ethernet: false Component.onCompleted: { Logger.log("Network", "Service started") @@ -43,6 +44,7 @@ Singleton { function refreshNetworks() { isLoading = true + checkEthernet.running = true existingNetwork.running = true } @@ -415,6 +417,24 @@ Singleton { } } } + + property Process checkEthernet: Process { + id: checkEthernet + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + stdout: StdioCollector { + onStreamFinished: { + var lines = text.split("\n") + for (var i = 0; i < lines.length; ++i) { + var parts = lines[i].split(":") + if (parts[1] === "ethernet" && parts[2] === "connected") { + root.ethernet = true + break + } + } + } + } + } property Process addConnectionProcess: Process { id: addConnectionProcess From 9528b10015b240ee81f80af6ebeaca01a0189d34 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 12:59:05 -0700 Subject: [PATCH 10/54] feat: power profile widget --- Commons/WidgetLoader.qml | 2 +- Modules/Bar/Widgets/PowerProfile.qml | 57 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 Modules/Bar/Widgets/PowerProfile.qml diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml index ce44431..17ef9bf 100644 --- a/Commons/WidgetLoader.qml +++ b/Commons/WidgetLoader.qml @@ -65,7 +65,7 @@ QtObject { // This is where you should add your Modules/Bar/Widgets/ // so it gets registered in the BarTab function discoverAvailableWidgets() { - const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] + const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] const availableWidgets = [] diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml new file mode 100644 index 0000000..ecef0ca --- /dev/null +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -0,0 +1,57 @@ +import QtQuick +import Quickshell +import Quickshell.Services.UPower +import QtQuick.Layouts +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + + property var powerProfiles: PowerProfiles + readonly property bool hasPP: powerProfiles.hasPerformanceProfile + + sizeMultiplier: 0.8 + visible: hasPP + + function profileIcon() { + if (!hasPP) + return "balance" + if (powerProfiles.profile === PowerProfile.Performance) + return "speed" + if (powerProfiles.profile === PowerProfile.Balanced) + return "balance" + if (powerProfiles.profile === PowerProfile.PowerSaver) + return "eco" + } + + function profileName() { + if (!hasPP) + return "Unknown" + if (powerProfiles.profile === PowerProfile.Performance) + return "Performance" + if (powerProfiles.profile === PowerProfile.Balanced) + return "Balanced" + if (powerProfiles.profile === PowerProfile.PowerSaver) + return "Power Saver" + } + + function changeProfile() { + if (!hasPP) return; + if (powerProfiles.profile === PowerProfile.Performance) + powerProfiles.profile = PowerProfile.Balanced + else if (powerProfiles.profile === PowerProfile.Balanced) + powerProfiles.profile = PowerProfile.PowerSaver + else if (powerProfiles.profile === PowerProfile.PowerSaver) + powerProfiles.profile = PowerProfile.Performance + } + + icon: root.profileIcon() + tooltipText: root.profileName() + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.transparent + onClicked: root.changeProfile() +} From 274fb8658a08b44319a1055ac4f2c8b810203ef7 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 22 Aug 2025 13:04:44 -0700 Subject: [PATCH 11/54] better profile switching order --- Modules/Bar/Widgets/PowerProfile.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index ecef0ca..6a9b978 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -40,11 +40,11 @@ NIconButton { function changeProfile() { if (!hasPP) return; if (powerProfiles.profile === PowerProfile.Performance) - powerProfiles.profile = PowerProfile.Balanced - else if (powerProfiles.profile === PowerProfile.Balanced) powerProfiles.profile = PowerProfile.PowerSaver - else if (powerProfiles.profile === PowerProfile.PowerSaver) + else if (powerProfiles.profile === PowerProfile.Balanced) powerProfiles.profile = PowerProfile.Performance + else if (powerProfiles.profile === PowerProfile.PowerSaver) + powerProfiles.profile = PowerProfile.Balanced } icon: root.profileIcon() From 245b2aa24a271174846654e0ae2a1c9b7d6d0fa1 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 16:51:30 -0400 Subject: [PATCH 12/54] IPC: powerMenu => powerPanel --- Modules/IPC/IPCManager.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index f4988d5..e8ac25c 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -75,7 +75,7 @@ Item { } IpcHandler { - target: "powerMenu" + target: "powerPanel" function toggle() { powerPanel.toggle(Quickshell.screens[0]) From 017813ab57c65cd1967e78ef98129df12dbf50b4 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 16:52:03 -0400 Subject: [PATCH 13/54] active window timer set in absolute ms --- Modules/Bar/Widgets/ActiveWindow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 70a0482..bfadc79 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -19,7 +19,7 @@ Row { // Timer to hide full title after window switch Timer { id: fullTitleTimer - interval: Style.animationSlow * 4 // Show full title for 2 seconds + interval: 2000 repeat: false onTriggered: { showingFullTitle = false From 2a6ad0819fa7b1f45420d3021c3e12efa0331b45 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 17:57:55 -0400 Subject: [PATCH 14/54] Fix NWidgetCard --- Modules/Bar/Widgets/MediaMini.qml | 4 +-- Modules/Bar/Widgets/PowerProfile.qml | 37 +++++++++++++------------ Modules/SettingsPanel/Tabs/AudioTab.qml | 6 ++-- Modules/SettingsPanel/Tabs/BarTab.qml | 5 +--- Modules/SidePanel/Cards/MediaCard.qml | 2 -- Widgets/NComboBox.qml | 2 +- Widgets/NWidgetCard.qml | 15 +++++----- 7 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 9875a4f..b884196 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -11,8 +11,8 @@ Row { id: root anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling - visible: MediaService.currentPlayer !== null - width: MediaService.currentPlayer !== null ? implicitWidth : 0 + visible: MediaService.currentPlayer !== null && MediaService.canPlay + width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index 6a9b978..7d3afa9 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -11,7 +11,7 @@ NIconButton { property var powerProfiles: PowerProfiles readonly property bool hasPP: powerProfiles.hasPerformanceProfile - + sizeMultiplier: 0.8 visible: hasPP @@ -25,26 +25,27 @@ NIconButton { if (powerProfiles.profile === PowerProfile.PowerSaver) return "eco" } - + function profileName() { - if (!hasPP) - return "Unknown" - if (powerProfiles.profile === PowerProfile.Performance) - return "Performance" - if (powerProfiles.profile === PowerProfile.Balanced) - return "Balanced" - if (powerProfiles.profile === PowerProfile.PowerSaver) - return "Power Saver" + if (!hasPP) + return "Unknown" + if (powerProfiles.profile === PowerProfile.Performance) + return "Performance" + if (powerProfiles.profile === PowerProfile.Balanced) + return "Balanced" + if (powerProfiles.profile === PowerProfile.PowerSaver) + return "Power Saver" } - + function changeProfile() { - if (!hasPP) return; - if (powerProfiles.profile === PowerProfile.Performance) - powerProfiles.profile = PowerProfile.PowerSaver - else if (powerProfiles.profile === PowerProfile.Balanced) - powerProfiles.profile = PowerProfile.Performance - else if (powerProfiles.profile === PowerProfile.PowerSaver) - powerProfiles.profile = PowerProfile.Balanced + if (!hasPP) + return + if (powerProfiles.profile === PowerProfile.Performance) + powerProfiles.profile = PowerProfile.PowerSaver + else if (powerProfiles.profile === PowerProfile.Balanced) + powerProfiles.profile = PowerProfile.Performance + else if (powerProfiles.profile === PowerProfile.PowerSaver) + powerProfiles.profile = PowerProfile.Balanced } icon: root.profileIcon() diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 2f64aca..065350f 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -308,7 +308,7 @@ ColumnLayout { Settings.data.audio.visualizerType = key } } - + NComboBox { label: "Frame Rate" description: "Target frame rate for audio visualizer. (default: 60)" @@ -344,8 +344,8 @@ ColumnLayout { } currentKey: Settings.data.audio.cavaFrameRate onSelected: key => { - Settings.data.audio.cavaFrameRate = key - } + Settings.data.audio.cavaFrameRate = key + } } } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 9e4bf9d..f6def60 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -33,7 +33,6 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -72,7 +71,7 @@ ColumnLayout { } } - ColumnLayout { + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -111,7 +110,6 @@ ColumnLayout { } } - NToggle { label: "Show Active Window's Icon" description: "Display the app icon next to the title of the currently focused window." @@ -130,7 +128,6 @@ ColumnLayout { } } - NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL * scaling diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index aad0839..fae3f38 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -160,8 +160,6 @@ NBox { height: 90 * scaling radius: width * 0.5 color: trackArt.visible ? Color.mPrimary : Color.transparent - border.color: trackArt.visible ? Color.mOutline : Color.transparent - border.width: Math.max(1, Style.borderS * scaling) clip: true NImageCircled { diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 1cb33bb..0a1c79b 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -15,7 +15,7 @@ ColumnLayout { property ListModel model: { } - property string currentKey: '' + property string currentKey: "" property string placeholder: "" signal selected(string key) diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml index 2808465..ccde0f2 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NWidgetCard.qml @@ -58,23 +58,24 @@ NCard { description: "" placeholder: "Add widget to " + sectionName.toLowerCase() + " section" onSelected: key => { - comboBox.selectedKey = key + comboBox.currentKey = key } + Layout.alignment: Qt.AlignVCenter } NIconButton { icon: "add" - size: 24 * scaling + colorBg: Color.mPrimary colorFg: Color.mOnPrimary - colorBgHover: Color.mPrimaryContainer - colorFgHover: Color.mOnPrimaryContainer + colorBgHover: Color.mSecondary + colorFgHover: Color.mOnSecondary enabled: comboBox.selectedKey !== "" Layout.alignment: Qt.AlignVCenter onClicked: { - if (comboBox.selectedKey !== "") { - addWidget(comboBox.selectedKey, sectionName.toLowerCase()) - comboBox.reset() + if (comboBox.currentKey !== "") { + addWidget(comboBox.currentKey, sectionName.toLowerCase()) + comboBox.currentKey = "" } } } From 6a159a390a21b64727ef85b3b078a012be66b2ff Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 18:36:45 -0400 Subject: [PATCH 15/54] Autoformattinmg --- Modules/Bar/Widgets/WiFi.qml | 3 ++- Services/NetworkService.qml | 2 +- Services/ScreenRecorderService.qml | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 193eae7..5b94a38 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -27,7 +27,8 @@ NIconButton { icon: { try { - if (NetworkService.ethernet) return "lan" + if (NetworkService.ethernet) + return "lan" let connected = false let signalStrength = 0 for (const net in NetworkService.networks) { diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 43e31e7..8dde4df 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -417,7 +417,7 @@ Singleton { } } } - + property Process checkEthernet: Process { id: checkEthernet running: false diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 57f66d9..5837314 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -33,16 +33,16 @@ Singleton { var flags = `-w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}` var command = ` _gpuscreenrecorder_flatpak_installed() { - flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder" + flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder" } if command -v gpu-screen-recorder >/dev/null 2>&1; then - gpu-screen-recorder ${flags} + gpu-screen-recorder ${flags} elif command -v flatpak >/dev/null 2>&1 && _gpuscreenrecorder_flatpak_installed; then - flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder ${flags} + flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder ${flags} else - notify-send "gpu-screen-recorder not installed!" -u critical - fi`; - + notify-send "gpu-screen-recorder not installed!" -u critical + fi` + //Logger.log("ScreenRecorder", command) Quickshell.execDetached(["sh", "-c", command]) Logger.log("ScreenRecorder", "Started recording") From 1b77f4882d4f2118623659f15be75c87b627cefc Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 18:37:48 -0400 Subject: [PATCH 16/54] Settings: reworked modular bar settings - deleted NCard, we already have NBox - assign a color to each widget for better overview when moving stuff around - minor QOL improvements to NComboBox --- Modules/SettingsPanel/Tabs/BarTab.qml | 3 +- Widgets/NCard.qml | 16 ---------- Widgets/NComboBox.qml | 13 ++++++-- Widgets/NWidgetCard.qml | 45 ++++++++++++++++++++------- 4 files changed, 46 insertions(+), 31 deletions(-) delete mode 100644 Widgets/NCard.qml diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index f6def60..a70986b 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -141,9 +141,10 @@ ColumnLayout { NText { text: "Widgets Positioning" - font.pointSize: Style.fontSizeL * scaling + font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface + Layout.bottomMargin: Style.marginS * scaling } NText { diff --git a/Widgets/NCard.qml b/Widgets/NCard.qml deleted file mode 100644 index a8f7cc0..0000000 --- a/Widgets/NCard.qml +++ /dev/null @@ -1,16 +0,0 @@ -import QtQuick -import qs.Commons -import qs.Services - -// Generic card container -Rectangle { - id: root - - implicitWidth: childrenRect.width - implicitHeight: childrenRect.height - - color: Color.mSurface - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) -} diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 0a1c79b..d2fdb8d 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,7 +8,7 @@ import qs.Widgets ColumnLayout { id: root - readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling + readonly property real preferredHeight: Style.baseWidgetSize * 1.35 * scaling property string label: "" property string description: "" @@ -39,7 +39,8 @@ ColumnLayout { ComboBox { id: combo - Layout.fillWidth: true + + Layout.preferredWidth: 320 * scaling Layout.preferredHeight: height model: model currentIndex: findIndexByKey(currentKey) @@ -128,5 +129,13 @@ ColumnLayout { radius: Style.radiusM * scaling } } + + // Update the currentIndex if the currentKey is changed externalyu + Connections { + target: root + function onCurrentKeyChanged() { + combo.currentIndex = root.findIndexByKey(currentKey) + } + } } } diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml index ccde0f2..2c9d00a 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NWidgetCard.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts import qs.Commons import qs.Widgets -NCard { +NBox { id: root property string sectionName: "" @@ -16,6 +16,7 @@ NCard { signal removeWidget(string section, int index) signal reorderWidget(string section, int fromIndex, int toIndex) + color: Color.mSurface Layout.fillWidth: true Layout.minimumHeight: { var widgetCount = widgetModel.length @@ -30,6 +31,25 @@ NCard { return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling } + // Generate widget color from name checksum + function getWidgetColor(name) { + const totalSum = name.split('').reduce((acc, character) => { + return acc + character.charCodeAt(0) + }, 0) + switch (totalSum % 5) { + case 0: + return Color.mPrimary + case 1: + return Color.mSecondary + case 2: + return Color.mTertiary + case 3: + return Color.mError + case 4: + return Color.mOnSurface + } + } + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginM * scaling @@ -42,21 +62,19 @@ NCard { text: sectionName + " Section" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold - color: Color.mOnSurface + color: Color.mSecondary Layout.alignment: Qt.AlignVCenter } Item { Layout.fillWidth: true } - NComboBox { id: comboBox - width: 120 * scaling model: availableWidgets label: "" description: "" - placeholder: "Add widget to " + sectionName.toLowerCase() + " section" + placeholder: "Add widget to the " + sectionName.toLowerCase() + " section..." onSelected: key => { comboBox.currentKey = key } @@ -75,7 +93,7 @@ NCard { onClicked: { if (comboBox.currentKey !== "") { addWidget(comboBox.currentKey, sectionName.toLowerCase()) - comboBox.currentKey = "" + comboBox.currentKey = "battery" } } } @@ -92,9 +110,9 @@ NCard { model: widgetModel delegate: Rectangle { width: widgetContent.implicitWidth + 16 * scaling - height: 48 * scaling - radius: Style.radiusS * scaling - color: Color.mPrimary + height: 40 * scaling + radius: Style.radiusL * scaling + color: root.getWidgetColor(modelData) border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -106,7 +124,8 @@ NCard { NIconButton { icon: "chevron_left" size: 20 * scaling - colorBg: Color.applyOpacity(Color.mOnPrimary, "20") + colorBorder: Color.applyOpacity(Color.mOutline, "40") + colorBg: Color.mOnSurface colorFg: Color.mOnPrimary colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") colorFgHover: Color.mOnPrimary @@ -128,7 +147,8 @@ NCard { NIconButton { icon: "chevron_right" size: 20 * scaling - colorBg: Color.applyOpacity(Color.mOnPrimary, "20") + colorBorder: Color.applyOpacity(Color.mOutline, "40") + colorBg: Color.mOnSurface colorFg: Color.mOnPrimary colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") colorFgHover: Color.mOnPrimary @@ -143,7 +163,8 @@ NCard { NIconButton { icon: "close" size: 20 * scaling - colorBg: Color.applyOpacity(Color.mOnPrimary, "20") + colorBorder: Color.applyOpacity(Color.mOutline, "40") + colorBg: Color.mOnSurface colorFg: Color.mOnPrimary colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") colorFgHover: Color.mOnPrimary From 765f6d5a1f77cfd333810c093ba2a8e35b9603c4 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 22 Aug 2025 18:51:27 -0400 Subject: [PATCH 17/54] Proper hiding of ActiveWindow when no window is active --- Modules/Bar/Widgets/ActiveWindow.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index bfadc79..8ca49dc 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -7,6 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets + Row { id: root anchors.verticalCenter: parent.verticalCenter @@ -62,6 +63,7 @@ Row { Rectangle { // Let the Rectangle size itself based on its content (the Row) + visible: root.visible width: row.width + Style.marginM * scaling * 2 height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) From 2029b5e1eb26451cef32746bd950f4a6eb9075a6 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 23 Aug 2025 12:16:08 +0200 Subject: [PATCH 18/54] Replace arrows in BarTab with drag&drop --- Modules/SettingsPanel/Tabs/BarTab.qml | 2 +- Widgets/NWidgetCard.qml | 188 +++++++++++++++++++++----- 2 files changed, 157 insertions(+), 33 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index a70986b..2811787 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -148,7 +148,7 @@ ColumnLayout { } NText { - text: "Add, remove, or reorder widgets in each section of the bar using the control buttons." + text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets." font.pointSize: Style.fontSizeXS * scaling color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml index 2c9d00a..92446af 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NWidgetCard.qml @@ -99,7 +99,9 @@ NBox { } } + // Drag and Drop Widget Area Flow { + id: widgetFlow Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 65 * scaling @@ -109,6 +111,10 @@ NBox { Repeater { model: widgetModel delegate: Rectangle { + id: widgetItem + required property int index + required property string modelData + width: widgetContent.implicitWidth + 16 * scaling height: 40 * scaling radius: Style.radiusL * scaling @@ -116,27 +122,31 @@ NBox { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) + // Drag properties + Drag.keys: ["widget"] + Drag.active: mouseArea.drag.active + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + + // Store the widget index for drag operations + property int widgetIndex: index + + // Visual feedback during drag + states: State { + when: mouseArea.drag.active + PropertyChanges { + target: widgetItem + scale: 1.1 + opacity: 0.9 + z: 1000 + } + } + RowLayout { id: widgetContent anchors.centerIn: parent spacing: Style.marginXS * scaling - NIconButton { - icon: "chevron_left" - size: 20 * scaling - colorBorder: Color.applyOpacity(Color.mOutline, "40") - colorBg: Color.mOnSurface - colorFg: Color.mOnPrimary - colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") - colorFgHover: Color.mOnPrimary - enabled: index > 0 - onClicked: { - if (index > 0) { - reorderWidget(sectionName.toLowerCase(), index, index - 1) - } - } - } - NText { text: modelData font.pointSize: Style.fontSizeS * scaling @@ -144,22 +154,6 @@ NBox { horizontalAlignment: Text.AlignHCenter } - NIconButton { - icon: "chevron_right" - size: 20 * scaling - colorBorder: Color.applyOpacity(Color.mOutline, "40") - colorBg: Color.mOnSurface - colorFg: Color.mOnPrimary - colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40") - colorFgHover: Color.mOnPrimary - enabled: index < widgetModel.length - 1 - onClicked: { - if (index < widgetModel.length - 1) { - reorderWidget(sectionName.toLowerCase(), index, index + 1) - } - } - } - NIconButton { icon: "close" size: 20 * scaling @@ -173,6 +167,136 @@ NBox { } } } + + // Mouse area for drag and drop + MouseArea { + id: mouseArea + anchors.fill: parent + drag.target: parent + + onPressed: { + Logger.log("NWidgetCard", `Started dragging widget: ${modelData} at index ${index}`) + // Bring to front when starting drag + widgetItem.z = 1000 + } + + onReleased: { + Logger.log("NWidgetCard", `Released widget: ${modelData} at index ${index}`) + // Reset z-index when drag ends + widgetItem.z = 0 + + // Get the global mouse position + const globalDropX = mouseArea.mouseX + widgetItem.x + widgetFlow.x + const globalDropY = mouseArea.mouseY + widgetItem.y + widgetFlow.y + + // Find which widget the drop position is closest to + let targetIndex = -1 + let minDistance = Infinity + + for (let i = 0; i < widgetModel.length; i++) { + if (i !== index) { + // Get the position of other widgets + const otherWidget = widgetFlow.children[i] + if (otherWidget && otherWidget.widgetIndex !== undefined) { + // Calculate the center of the other widget + const otherCenterX = otherWidget.x + otherWidget.width / 2 + widgetFlow.x + const otherCenterY = otherWidget.y + otherWidget.height / 2 + widgetFlow.y + + // Calculate distance to the center of this widget + const distance = Math.sqrt( + Math.pow(globalDropX - otherCenterX, 2) + + Math.pow(globalDropY - otherCenterY, 2) + ) + + if (distance < minDistance) { + minDistance = distance + targetIndex = otherWidget.widgetIndex + } + } + } + } + + // Only reorder if we found a valid target and it's different from current position + if (targetIndex !== -1 && targetIndex !== index) { + const fromIndex = index + const toIndex = targetIndex + Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed(2)})`) + reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) + } else { + Logger.log("NWidgetCard", `No valid drop target found for widget at index ${index}`) + } + } + } + } + } + } + + // Drop zone at the beginning (positioned absolutely) + DropArea { + id: startDropZone + width: 40 * scaling + height: 40 * scaling + x: widgetFlow.x + y: widgetFlow.y + (widgetFlow.height - height) / 2 + keys: ["widget"] + z: 1001 // Above the Flow + + Rectangle { + anchors.fill: parent + color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent + border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent + border.width: startDropZone.containsDrag ? 2 : 0 + radius: Style.radiusS * scaling + } + + onEntered: function(drag) { + Logger.log("NWidgetCard", "Entered start drop zone") + } + + onDropped: function(drop) { + Logger.log("NWidgetCard", "Dropped on start zone") + if (drop.source && drop.source.widgetIndex !== undefined) { + const fromIndex = drop.source.widgetIndex + const toIndex = 0 // Insert at the beginning + if (fromIndex !== toIndex) { + Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to beginning`) + reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) + } + } + } + } + + // Drop zone at the end (positioned absolutely) + DropArea { + id: endDropZone + width: 40 * scaling + height: 40 * scaling + x: widgetFlow.x + widgetFlow.width - width + y: widgetFlow.y + (widgetFlow.height - height) / 2 + keys: ["widget"] + z: 1001 // Above the Flow + + Rectangle { + anchors.fill: parent + color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent + border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent + border.width: endDropZone.containsDrag ? 2 : 0 + radius: Style.radiusS * scaling + } + + onEntered: function(drag) { + Logger.log("NWidgetCard", "Entered end drop zone") + } + + onDropped: function(drop) { + Logger.log("NWidgetCard", "Dropped on end zone") + if (drop.source && drop.source.widgetIndex !== undefined) { + const fromIndex = drop.source.widgetIndex + const toIndex = widgetModel.length // Insert at the end + if (fromIndex !== toIndex) { + Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to end`) + reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) + } } } } From ca7e0cc105b4df5f32ea77800ca1adbea46f7da2 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 23 Aug 2025 13:02:37 +0200 Subject: [PATCH 19/54] Add KeyboardLayoutService, add KeyboardLayout Widget, add KeyboardLayout to LockScreen --- Commons/WidgetLoader.qml | 2 +- Modules/Bar/Widgets/KeyboardLayout.qml | 34 ++++++++ Modules/LockScreen/LockScreen.qml | 26 ++++++ Services/KeyboardLayoutService.qml | 112 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 Modules/Bar/Widgets/KeyboardLayout.qml create mode 100644 Services/KeyboardLayoutService.qml diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml index 17ef9bf..872f64d 100644 --- a/Commons/WidgetLoader.qml +++ b/Commons/WidgetLoader.qml @@ -65,7 +65,7 @@ QtObject { // This is where you should add your Modules/Bar/Widgets/ // so it gets registered in the BarTab function discoverAvailableWidgets() { - const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] + const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "KeyboardLayout", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] const availableWidgets = [] diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml new file mode 100644 index 0000000..fd1e4fb --- /dev/null +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -0,0 +1,34 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + id: root + + width: pill.width + height: pill.height + + // Use the shared service for keyboard layout + property string currentLayout: KeyboardLayoutService.currentLayout + + NPill { + id: pill + icon: "keyboard_alt" + iconCircleColor: Color.mPrimary + collapsedIconColor: Color.mOnSurface + autoHide: false // Important to be false so we can hover as long as we want + text: currentLayout + tooltipText: "Keyboard Layout: " + currentLayout + + onClicked: { + // You could open keyboard settings here if needed + // For now, just show the current layout + } + } + + +} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 5f41bd7..f5ded6e 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -163,6 +163,13 @@ Loader { } } + // Keyboard layout indicator component + Item { + id: keyboardLayout + + property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" + } + // Wallpaper image Image { id: lockBgImage @@ -580,6 +587,25 @@ Loader { font.weight: Style.fontWeightBold } } + + // Keyboard layout indicator + Row { + spacing: Style.marginS * scaling + + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + + NIcon { + text: "keyboard_alt" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + } + } } } diff --git a/Services/KeyboardLayoutService.qml b/Services/KeyboardLayoutService.qml new file mode 100644 index 0000000..c66e5c9 --- /dev/null +++ b/Services/KeyboardLayoutService.qml @@ -0,0 +1,112 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs.Commons +import qs.Services + +Singleton { + id: root + + property string currentLayout: "Unknown" + property int updateInterval: 1000 // Update every second + + // Timer to periodically update the layout + Timer { + id: updateTimer + interval: updateInterval + running: true + repeat: true + onTriggered: { + updateLayout() + } + } + + // Process to get current keyboard layout using niri msg (Wayland native) + Process { + id: niriLayoutProcess + running: false + command: ["niri", "msg", "-j", "keyboard-layouts"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + const layoutName = data.names[data.current_idx] + root.currentLayout = mapLayoutNameToCode(layoutName) + } catch (e) { + root.currentLayout = "Unknown" + } + } + } + } + + // Process to get current keyboard layout using hyprctl (Hyprland) + Process { + id: hyprlandLayoutProcess + running: false + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { + try { + const data = JSON.parse(text) + // Find the main keyboard and get its active keymap + const mainKeyboard = data.keyboards.find(kb => kb.main === true) + if (mainKeyboard && mainKeyboard.active_keymap) { + root.currentLayout = mapLayoutNameToCode(mainKeyboard.active_keymap) + } else { + root.currentLayout = "Unknown" + } + } catch (e) { + root.currentLayout = "Unknown" + } + } + } + } + + // Layout name to ISO code mapping + property var layoutMap: { + "German": "de", + "English (US)": "us", + "English (UK)": "gb", + "French": "fr", + "Spanish": "es", + "Italian": "it", + "Portuguese (Brazil)": "br", + "Portuguese": "pt", + "Russian": "ru", + "Polish": "pl", + "Swedish": "se", + "Norwegian": "no", + "Danish": "dk", + "Finnish": "fi", + "Hungarian": "hu", + "Turkish": "tr", + "Czech": "cz", + "Slovak": "sk", + "Japanese": "jp", + "Korean": "kr", + "Chinese": "cn" + } + + // Map layout names to ISO codes + function mapLayoutNameToCode(layoutName) { + return layoutMap[layoutName] || layoutName // fallback to raw name if not found + } + + Component.onCompleted: { + Logger.log("KeyboardLayout", "Service started") + updateLayout() + } + + function updateLayout() { + if (CompositorService.isHyprland) { + hyprlandLayoutProcess.running = true + } else if (CompositorService.isNiri) { + niriLayoutProcess.running = true + } else { + currentLayout = "Unknown" + } + } +} From 374d3681ce3aa13c0b1bd87ffa03aa74a37ea0b4 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 23 Aug 2025 15:07:54 +0200 Subject: [PATCH 20/54] Fix ActiveWindow Title change (#128) --- Modules/Bar/Widgets/ActiveWindow.qml | 17 ++++++++++++++--- Services/CompositorService.qml | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 8ca49dc..afcd724 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -27,7 +27,7 @@ Row { } } - // Update text when window changes + // Update text when window changes or title changes Connections { target: CompositorService function onActiveWindowChanged() { @@ -38,11 +38,22 @@ Row { fullTitleTimer.restart() } } + + function onWindowTitleChanged() { + // Direct response to title changes + if (CompositorService.focusedWindowIndex === lastWindowIndex) { + // Same window, title changed - show full title briefly + showingFullTitle = true + fullTitleTimer.restart() + } + } } function getTitle() { - const focusedWindow = CompositorService.getFocusedWindow() - return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : "" + // Use the service's focusedWindowTitle property which is updated immediately + // when WindowOpenedOrChanged events are received + return CompositorService.focusedWindowTitle !== "(No active window)" ? + CompositorService.focusedWindowTitle : "" } function getAppIcon() { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index f657cb1..595f153 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -27,6 +27,7 @@ Singleton { signal activeWindowChanged signal overviewStateChanged signal windowListChanged + signal windowTitleChanged // Compositor detection Component.onCompleted: { @@ -297,6 +298,8 @@ Singleton { "isFocused": windowData.is_focused === true } + + if (existingIndex >= 0) { // Update existing window windows[existingIndex] = newWindow @@ -311,6 +314,11 @@ Singleton { focusedWindowIndex = windows.findIndex(w => w.id === windowData.id) updateFocusedWindowTitle() activeWindowChanged() + } else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) { + // If this is the currently focused window (but not newly focused), + // still update the title in case it changed + updateFocusedWindowTitle() + activeWindowChanged() } windowListChanged() @@ -449,11 +457,17 @@ Singleton { } function updateFocusedWindowTitle() { + const oldTitle = focusedWindowTitle if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" } else { focusedWindowTitle = "(No active window)" } + + // Emit signal if title actually changed + if (oldTitle !== focusedWindowTitle) { + windowTitleChanged() + } } // Generic workspace switching From e04c4e13940662d130849af68cc5980fbb84598f Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 23 Aug 2025 15:41:19 +0200 Subject: [PATCH 21/54] Small change to ActiveWindow --- Modules/Bar/Widgets/ActiveWindow.qml | 11 +---------- Services/CompositorService.qml | 10 +++++++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index afcd724..990f532 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -27,7 +27,7 @@ Row { } } - // Update text when window changes or title changes + // Update text when window changes Connections { target: CompositorService function onActiveWindowChanged() { @@ -38,15 +38,6 @@ Row { fullTitleTimer.restart() } } - - function onWindowTitleChanged() { - // Direct response to title changes - if (CompositorService.focusedWindowIndex === lastWindowIndex) { - // Same window, title changed - show full title briefly - showingFullTitle = true - fullTitleTimer.restart() - } - } } function getTitle() { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 595f153..aaddde4 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -311,14 +311,18 @@ Singleton { // Update focused window index if this window is focused if (newWindow.isFocused) { + const oldFocusedIndex = focusedWindowIndex focusedWindowIndex = windows.findIndex(w => w.id === windowData.id) updateFocusedWindowTitle() - activeWindowChanged() + + // Only emit activeWindowChanged if the focused window actually changed + if (oldFocusedIndex !== focusedWindowIndex) { + activeWindowChanged() + } } else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) { // If this is the currently focused window (but not newly focused), - // still update the title in case it changed + // still update the title in case it changed, but don't emit activeWindowChanged updateFocusedWindowTitle() - activeWindowChanged() } windowListChanged() From b85d68f4cbd7ca6232af474eb6b8c70101094ba2 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 23 Aug 2025 16:03:23 +0200 Subject: [PATCH 22/54] Fix modular bar (remove option) --- Modules/SettingsPanel/Tabs/BarTab.qml | 13 +++++++++++++ Widgets/NWidgetCard.qml | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 2811787..906d958 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -218,6 +218,8 @@ ColumnLayout { function removeWidgetFromSection(section, index) { console.log("Removing widget from section", section, "at index", index) var sectionArray = Settings.data.bar.widgets[section] + console.log("Current section array:", JSON.stringify(sectionArray)) + if (sectionArray && index >= 0 && index < sectionArray.length) { // Create a new array to avoid modifying the original var newArray = sectionArray.slice() @@ -226,6 +228,17 @@ ColumnLayout { // Assign the new array Settings.data.bar.widgets[section] = newArray + + // Force a settings save + console.log("Settings updated, triggering save...") + + // Verify the change was applied + Qt.setTimeout(function() { + var updatedArray = Settings.data.bar.widgets[section] + console.log("Verification - updated section array:", JSON.stringify(updatedArray)) + }, 100) + } else { + console.log("Invalid section or index:", section, index, "array length:", sectionArray ? sectionArray.length : "null") } } diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml index 92446af..22f4f5a 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NWidgetCard.qml @@ -175,6 +175,19 @@ NBox { drag.target: parent onPressed: { + // Check if the click is on the close button area + const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling + const closeButtonY = widgetContent.y + const closeButtonWidth = 20 * scaling + const closeButtonHeight = 20 * scaling + + if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth && + mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) { + // Click is on the close button, don't start drag + mouse.accepted = false + return + } + Logger.log("NWidgetCard", `Started dragging widget: ${modelData} at index ${index}`) // Bring to front when starting drag widgetItem.z = 1000 From 57731e5a369bfd3fcf55e2dd3c81821bc0664fdf Mon Sep 17 00:00:00 2001 From: wer-zen Date: Sat, 23 Aug 2025 16:43:36 +0200 Subject: [PATCH 23/54] Settings.qml cleanup --- Commons/Settings.qml | 490 ++++++++++++++++++++----------------------- 1 file changed, 229 insertions(+), 261 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index accd50b..02554f3 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -1,280 +1,248 @@ +pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services -pragma Singleton Singleton { - id: root + id: root - // Define our app directories - // Default config directory: ~/.config/noctalia - // Default cache directory: ~/.cache/noctalia - property string shellName: "noctalia" - property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") - || Quickshell.env( - "HOME") + "/.config") + "/" + shellName + "/" - property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( - "HOME") + "/.cache") + "/" + shellName + "/" - property string cacheDirImages: cacheDir + "images/" + // Define our app directories + // Default config directory: ~/.config/noctalia + // Default cache directory: ~/.cache/noctalia + property string shellName: "noctalia" + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/" + property string cacheDirImages: cacheDir + "images/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") - property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") - property string defaultAvatar: Quickshell.env("HOME") + "/.face" + property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") + property string defaultAvatar: Quickshell.env("HOME") + "/.face" - // Used to access via Settings.data.xxx.yyy - property alias data: adapter + // Used to access via Settings.data.xxx.yyy + property alias data: adapter - // Flag to prevent unnecessary wallpaper calls during reloads - property bool isInitialLoad: true + // Flag to prevent unnecessary wallpaper calls during reloads + property bool isInitialLoad: true - // Function to validate monitor configurations - function validateMonitorConfigurations() { - var availableScreenNames = [] - for (var i = 0; i < Quickshell.screens.length; i++) { - availableScreenNames.push(Quickshell.screens[i].name) - } - - Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]") - Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]") - - // Check bar monitors - if (adapter.bar.monitors.length > 0) { - var hasValidBarMonitor = false - for (var j = 0; j < adapter.bar.monitors.length; j++) { - if (availableScreenNames.includes(adapter.bar.monitors[j])) { - hasValidBarMonitor = true - break - } - } - if (!hasValidBarMonitor) { - Logger.log("Settings", - "No configured bar monitors found on system, clearing bar monitor list to show on all screens") - adapter.bar.monitors = [] - } else { - Logger.log("Settings", "Found valid bar monitors, keeping configuration") - } - } else { - Logger.log("Settings", "Bar monitor list is empty, will show on all available screens") - } - } - Item { - Component.onCompleted: { - - // ensure settings dir exists - Quickshell.execDetached(["mkdir", "-p", configDir]) - Quickshell.execDetached(["mkdir", "-p", cacheDir]) - Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) - } - } - - // Don't write settings to disk immediately - // This avoid excessive IO when a variable changes rapidly (ex: sliders) - Timer { - id: saveTimer - running: false - interval: 1000 - onTriggered: settingsFileView.writeAdapter() - } - - FileView { - id: settingsFileView - path: settingsFile - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: saveTimer.start() - Component.onCompleted: function () { - reload() - } - onLoaded: function () { - Qt.callLater(function () { - if (isInitialLoad) { - Logger.log("Settings", "OnLoaded") - // Only set wallpaper on initial load, not on reloads - if (adapter.wallpaper.current !== "") { - Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) - WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) - } - - // Validate monitor configurations, only once - // if none of the configured monitors exist, clear the lists - validateMonitorConfigurations() + // Function to validate monitor configurations + function validateMonitorConfigurations() { + var availableScreenNames = []; + for (var i = 0; i < Quickshell.screens.length; i++) { + availableScreenNames.push(Quickshell.screens[i].name); } - isInitialLoad = false - }) - } - onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) - // File doesn't exist, create it with default values - writeAdapter() - } + Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]"); + Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]"); - JsonAdapter { - id: adapter - - // bar - property JsonObject bar - - bar: JsonObject { - property string position: "top" // Possible values: "top", "bottom" - property bool showActiveWindowIcon: true - property bool alwaysShowBatteryPercentage: false - property real backgroundOpacity: 1.0 - property list monitors: [] - - // Widget configuration for modular bar system - property JsonObject widgets - widgets: JsonObject { - property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] - property list center: ["Workspace"] - property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] + // Check bar monitors + if (adapter.bar.monitors.length > 0) { + var hasValidBarMonitor = false; + for (var j = 0; j < adapter.bar.monitors.length; j++) { + if (availableScreenNames.includes(adapter.bar.monitors[j])) { + hasValidBarMonitor = true; + break; + } + } + if (!hasValidBarMonitor) { + Logger.log("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens"); + adapter.bar.monitors = []; + } else { + Logger.log("Settings", "Found valid bar monitors, keeping configuration"); + } + } else { + Logger.log("Settings", "Bar monitor list is empty, will show on all available screens"); + } + } + Item { + Component.onCompleted: { + + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]); + Quickshell.execDetached(["mkdir", "-p", cacheDir]); + Quickshell.execDetached(["mkdir", "-p", cacheDirImages]); + } + } + + // Don't write settings to disk immediately + // This avoid excessive IO when a variable changes rapidly (ex: sliders) + Timer { + id: saveTimer + running: false + interval: 1000 + onTriggered: settingsFileView.writeAdapter() + } + + FileView { + id: settingsFileView + path: settingsFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: saveTimer.start() + Component.onCompleted: function () { + reload(); + } + onLoaded: function () { + Qt.callLater(function () { + if (isInitialLoad) { + Logger.log("Settings", "OnLoaded"); + // Only set wallpaper on initial load, not on reloads + if (adapter.wallpaper.current !== "") { + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current); + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true); + } + + // Validate monitor configurations, only once + // if none of the configured monitors exist, clear the lists + validateMonitorConfigurations(); + } + + isInitialLoad = false; + }); + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) + // File doesn't exist, create it with default values + writeAdapter(); + } + + JsonAdapter { + id: adapter + + // bar + property JsonObject bar: JsonObject { + property string position: "top" // Possible values: "top", "bottom" + property bool showActiveWindowIcon: true + property bool alwaysShowBatteryPercentage: false + property real backgroundOpacity: 1.0 + property list monitors: [] + + // Widget configuration for modular bar system + property JsonObject widgets + widgets: JsonObject { + property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] + property list center: ["Workspace"] + property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] + } + } + + // general + property JsonObject general: JsonObject { + property string avatarImage: defaultAvatar + property bool dimDesktop: false + property bool showScreenCorners: false + property real radiusRatio: 1.0 + } + + // location + property JsonObject location: JsonObject { + property string name: "Tokyo" + property bool useFahrenheit: false + property bool reverseDayMonth: false + property bool use12HourClock: false + property bool showDateWithClock: false + } + + // screen recorder + property JsonObject screenRecorder: JsonObject { + property string directory: "~/Videos" + property int frameRate: 60 + property string audioCodec: "opus" + property string videoCodec: "h264" + property string quality: "very_high" + property string colorRange: "limited" + property bool showCursor: true + property string audioSource: "default_output" + property string videoSource: "portal" + } + + // wallpaper + property JsonObject wallpaper: JsonObject { + property string directory: "/usr/share/wallpapers" + property string current: "" + property bool isRandom: false + property int randomInterval: 300 + property JsonObject swww + + onDirectoryChanged: WallpaperService.listWallpapers() + onIsRandomChanged: WallpaperService.toggleRandomWallpaper() + onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() + + swww: JsonObject { + property bool enabled: false + property string resizeMethod: "crop" + property int transitionFps: 60 + property string transitionType: "random" + property real transitionDuration: 1.1 + } + } + + // applauncher + property JsonObject appLauncher: JsonObject { + // When disabled, Launcher hides clipboard command and ignores cliphist + property bool enableClipboardHistory: true + // Position: center, top_left, top_right, bottom_left, bottom_right + property string position: "center" + property list pinnedExecs: [] + } + + // dock + property JsonObject dock: JsonObject { + property bool autoHide: false + property bool exclusive: false + property list monitors: [] + } + + // network + property JsonObject network: JsonObject { + property bool wifiEnabled: true + property bool bluetoothEnabled: true + } + + // notifications + property JsonObject notifications: JsonObject { + property list monitors: [] + } + + // audio + property JsonObject audio: JsonObject { + property bool showMiniplayerAlbumArt: false + property bool showMiniplayerCava: false + property string visualizerType: "linear" + property int volumeStep: 5 + property int cavaFrameRate: 60 + } + + // ui + property JsonObject ui: JsonObject { + property string fontDefault: "Roboto" // Default font for all text + property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal + property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays + + // Legacy compatibility + property string fontFamily: fontDefault // Keep for backward compatibility + + // Idle inhibitor state + property bool idleInhibitorEnabled: false + } + + // Scaling (not stored inside JsonObject, or it crashes) + property var monitorsScaling: {} + + // brightness + property JsonObject brightness: JsonObject { + property int brightnessStep: 5 + } + + property JsonObject colorSchemes: JsonObject { + property bool useWallpaperColors: false + property string predefinedScheme: "" + property bool darkMode: true + // External app theming (GTK & Qt) + property bool themeApps: false + } } - } - - // general - property JsonObject general - - general: JsonObject { - property string avatarImage: defaultAvatar - property bool dimDesktop: false - property bool showScreenCorners: false - property real radiusRatio: 1.0 - } - - // location - property JsonObject location - - location: JsonObject { - property string name: "Tokyo" - property bool useFahrenheit: false - property bool reverseDayMonth: false - property bool use12HourClock: false - property bool showDateWithClock: false - } - - // screen recorder - property JsonObject screenRecorder - - screenRecorder: JsonObject { - property string directory: "~/Videos" - property int frameRate: 60 - property string audioCodec: "opus" - property string videoCodec: "h264" - property string quality: "very_high" - property string colorRange: "limited" - property bool showCursor: true - property string audioSource: "default_output" - property string videoSource: "portal" - } - - // wallpaper - property JsonObject wallpaper - - wallpaper: JsonObject { - property string directory: "/usr/share/wallpapers" - property string current: "" - property bool isRandom: false - property int randomInterval: 300 - property JsonObject swww - - onDirectoryChanged: WallpaperService.listWallpapers() - onIsRandomChanged: WallpaperService.toggleRandomWallpaper() - onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() - - swww: JsonObject { - property bool enabled: false - property string resizeMethod: "crop" - property int transitionFps: 60 - property string transitionType: "random" - property real transitionDuration: 1.1 - } - } - - // applauncher - property JsonObject appLauncher - - appLauncher: JsonObject { - // When disabled, Launcher hides clipboard command and ignores cliphist - property bool enableClipboardHistory: true - // Position: center, top_left, top_right, bottom_left, bottom_right - property string position: "center" - property list pinnedExecs: [] - } - - // dock - property JsonObject dock - - dock: JsonObject { - property bool autoHide: false - property bool exclusive: false - property list monitors: [] - } - - // network - property JsonObject network - - network: JsonObject { - property bool wifiEnabled: true - property bool bluetoothEnabled: true - } - - // notifications - property JsonObject notifications - - notifications: JsonObject { - property list monitors: [] - } - - // audio - property JsonObject audio - - audio: JsonObject { - property bool showMiniplayerAlbumArt: false - property bool showMiniplayerCava: false - property string visualizerType: "linear" - property int volumeStep: 5 - property int cavaFrameRate: 60 - } - - // ui - property JsonObject ui - - ui: JsonObject { - property string fontDefault: "Roboto" // Default font for all text - property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal - property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays - - // Legacy compatibility - property string fontFamily: fontDefault // Keep for backward compatibility - - // Idle inhibitor state - property bool idleInhibitorEnabled: false - } - - // Scaling (not stored inside JsonObject, or it crashes) - property var monitorsScaling: { - - } - - // brightness - property JsonObject brightness - - brightness: JsonObject { - property int brightnessStep: 5 - } - - property JsonObject colorSchemes - - colorSchemes: JsonObject { - property bool useWallpaperColors: false - property string predefinedScheme: "" - property bool darkMode: true - // External app theming (GTK & Qt) - property bool themeApps: false - } } - } } From 85e9031df865c9d46817b2fc3d5b484a9f8689b5 Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 00:49:41 +0300 Subject: [PATCH 24/54] Add ArchUpdater widget and service; update settings for Arch updates --- Commons/Settings.qml | 3 +- Commons/WidgetLoader.qml | 2 +- Modules/Bar/Widgets/ArchUpdater.qml | 49 ++++++ Modules/SettingsPanel/Tabs/BarTab.qml | 11 ++ Services/ArchUpdaterService.qml | 222 ++++++++++++++++++++++++++ 5 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 Modules/Bar/Widgets/ArchUpdater.qml create mode 100644 Services/ArchUpdaterService.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index accd50b..3600c78 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -123,6 +123,7 @@ Singleton { property bool showActiveWindowIcon: true property bool alwaysShowBatteryPercentage: false property real backgroundOpacity: 1.0 + property bool showArchUpdater: true property list monitors: [] // Widget configuration for modular bar system @@ -130,7 +131,7 @@ Singleton { widgets: JsonObject { property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] property list center: ["Workspace"] - property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] + property list right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] } } diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml index 872f64d..0a2b01c 100644 --- a/Commons/WidgetLoader.qml +++ b/Commons/WidgetLoader.qml @@ -65,7 +65,7 @@ QtObject { // This is where you should add your Modules/Bar/Widgets/ // so it gets registered in the BarTab function discoverAvailableWidgets() { - const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "KeyboardLayout", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] + const widgetFiles = ["ActiveWindow", "ArchUpdater", "Battery", "Bluetooth", "Brightness", "Clock", "KeyboardLayout", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] const availableWidgets = [] diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml new file mode 100644 index 0000000..66378ff --- /dev/null +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -0,0 +1,49 @@ +import qs.Commons +import qs.Services +import qs.Widgets + +NIconButton { + id: root + visible: Settings.data.bar.showArchUpdater && ArchUpdaterService.isArchBased + sizeMultiplier: 0.8 + + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.transparent + + icon: !ArchUpdaterService.ready ? "block" : (ArchUpdaterService.busy ? "sync" : (ArchUpdaterService.updatePackages.length > 0 ? "system_update" : "task_alt")) + + tooltipText: { + if (!ArchUpdaterService.isArchBased) + return "Arch users already ran 'sudo pacman -Syu' for breakfast."; + if (!ArchUpdaterService.checkupdatesAvailable) + return "Please install pacman-contrib to use this feature."; + if (ArchUpdaterService.busy) + return "Checking for updates…"; + + var count = ArchUpdaterService.updatePackages.length; + if (count === 0) + return "No updates available"; + + var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:"); + + var list = ArchUpdaterService.updatePackages || []; + var s = ""; + var limit = Math.min(list.length, 10); + for (var i = 0; i < limit; ++i) { + var p = list[i]; + s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion); + } + if (list.length > 10) + s += "\n… and " + (list.length - 10) + " more"; + + return header + "\n" + s; + } + + onClicked: { + if (!ArchUpdaterService.ready || ArchUpdaterService.busy) + return; + ArchUpdaterService.runUpdate(); + } +} diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 906d958..214408e 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -127,6 +127,17 @@ ColumnLayout { Settings.data.bar.alwaysShowBatteryPercentage = checked } } + + NToggle { + visible: ArchUpdaterService.isArchBased + label: "Show Arch Updater" + description: "Show the Arch Linux updates widget." + checked: Settings.data.bar.showArchUpdater + onToggled: checked => { + Settings.data.bar.showArchUpdater = checked + } + } + NDivider { Layout.fillWidth: true diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml new file mode 100644 index 0000000..dbe9459 --- /dev/null +++ b/Services/ArchUpdaterService.qml @@ -0,0 +1,222 @@ +pragma Singleton +import Quickshell +import QtQuick +import Quickshell.Io +import qs.Commons + +Singleton { + id: updateService + property bool isArchBased: false + property bool checkupdatesAvailable: false + readonly property bool ready: isArchBased && checkupdatesAvailable + readonly property bool busy: pkgProc.running + readonly property int updates: updatePackages.length + property var updatePackages: [] + property double lastSync: 0 + property bool lastWasFull: false + property int failureCount: 0 + readonly property int failureThreshold: 5 + readonly property int quickTimeoutMs: 12 * 1000 + readonly property int minuteMs: 60 * 1000 + readonly property int pollInterval: 1 * minuteMs + readonly property int syncInterval: 15 * minuteMs + property int lastNotifiedUpdates: 0 + + property var updateCommand: ["xdg-terminal-exec", "--title=System Updates", "-e", "sh", "-c", "sudo pacman -Syu; printf '\n\nUpdate finished. Press Enter to exit...'; read _"] + + PersistentProperties { + id: cache + reloadableId: "ArchCheckerCache" + + property string cachedUpdatePackagesJson: "[]" + property double cachedLastSync: 0 + } + + Component.onCompleted: { + const persisted = JSON.parse(cache.cachedUpdatePackagesJson || "[]"); + if (persisted.length) + updatePackages = _clonePackageList(persisted); + if (cache.cachedLastSync > 0) + lastSync = cache.cachedLastSync; + } + + function runUpdate() { + if (updates > 0) { + Quickshell.execDetached(updateCommand); + } else { + doPoll(true); + } + } + + function notify(title, body) { + const app = "UpdateService"; + const icon = "system-software-update"; + Quickshell.execDetached(["notify-send", "-a", app, "-i", icon, String(title || ""), String(body || "")]); + } + + function startUpdateProcess(cmd) { + pkgProc.command = cmd; + pkgProc.running = true; + killTimer.interval = lastWasFull ? 60 * 1000 : minuteMs; + killTimer.restart(); + } + + function doPoll(forceFull = false) { + if (busy) + return; + const full = forceFull || (Date.now() - lastSync > syncInterval); + lastWasFull = full; + + pkgProc.command = full ? ["checkupdates", "--nocolor"] : ["checkupdates", "--nosync", "--nocolor"]; + pkgProc.running = true; + killTimer.restart(); + } + + Process { + id: pacmanCheck + running: true + command: ["sh", "-c", "p=$(command -v pacman >/dev/null && echo yes || echo no); c=$(command -v checkupdates >/dev/null && echo yes || echo no); echo \"$p $c\""] + stdout: StdioCollector { + onStreamFinished: { + const parts = (text || "").trim().split(/\s+/); + updateService.isArchBased = (parts[0] === "yes"); + updateService.checkupdatesAvailable = (parts[1] === "yes"); + if (updateService.ready) { + updateService.doPoll(); + pollTimer.start(); + } + } + } + } + + Process { + id: pkgProc + onExited: function (exitCode, exitStatus) { + killTimer.stop(); + if (exitCode !== 0 && exitCode !== 2) { + updateService.failureCount++; + Logger.warn("UpdateService", `checkupdates failed (code: ${exitCode}, status: ${exitStatus})`); + if (updateService.failureCount >= updateService.failureThreshold) { + updateService.notify(qsTr("Update check failed"), qsTr(`Exit code: ${exitCode} (failed ${updateService.failureCount} times)`)); + updateService.failureCount = 0; + } + updateService.updatePackages = []; + } + } + + stdout: StdioCollector { + id: out + onStreamFinished: { + if (!pkgProc.running || updateService.busy) + return; + killTimer.stop(); + + const parsed = updateService._parseUpdateOutput(out.text); + updateService.updatePackages = parsed.pkgs; + + if (updateService.lastWasFull) { + updateService.lastSync = Date.now(); + } + + cache.cachedUpdatePackagesJson = JSON.stringify(updateService._clonePackageList(updateService.updatePackages)); + cache.cachedLastSync = updateService.lastSync; + updateService._summarizeAndNotify(parsed.pkgs, updateService.updates); + } + } + stderr: StdioCollector { + id: err + onStreamFinished: { + const stderrText = (err.text || "").trim(); + if (stderrText) { + Logger.warn("UpdateService", "stderr:", stderrText); + updateService.failureCount++; + updateService._notifyOnFailureThreshold(stderrText); + } else { + updateService.failureCount = 0; + } + } + } + } + + function _notifyOnFailureThreshold(body) { + if (failureCount >= failureThreshold) { + notify(qsTr("Update check failed"), String(body || "")); + failureCount = 0; + return true; + } + return false; + } + + function _clonePackageList(list) { + const src = Array.isArray(list) ? list : []; + return src.map(p => ({ + name: String(p.name || ""), + oldVersion: String(p.oldVersion || ""), + newVersion: String(p.newVersion || "") + })); + } + + function _parseUpdateOutput(rawText) { + const raw = (rawText || "").trim(); + const lines = raw ? raw.split(/\r?\n/) : []; + const pkgs = []; + for (let i = 0; i < lines.length; ++i) { + const m = lines[i].match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/); + if (m) { + pkgs.push({ + name: m[1], + oldVersion: m[2], + newVersion: m[3] + }); + } + } + return { + raw, + pkgs + }; + } + + function _summarizeAndNotify() { + if (updates === 0) { + lastNotifiedUpdates = 0; + return; + } + if (updates <= lastNotifiedUpdates) + return; + const added = updates - lastNotifiedUpdates; + const msg = added === 1 ? qsTr("One new package can be upgraded (") + updates + qsTr(")") : `${added} ${qsTr("new packages can be upgraded (")} ${updates} ${qsTr(")")}`; + notify(qsTr("Updates Available"), msg); + lastNotifiedUpdates = updates; + } + + Timer { + id: pollTimer + interval: updateService.pollInterval + repeat: true + onTriggered: { + if (!updateService.ready) + return; + updateService.doPoll(); + } + } + + Timer { + id: killTimer + interval: updateService.lastWasFull ? updateService.minuteMs : updateService.quickTimeoutMs + repeat: false + onTriggered: { + if (pkgProc.running) { + Logger.error("UpdateService", "Update check killed (timeout)"); + updateService.notify(qsTr("Update check killed"), qsTr("Process took too long")); + } + } + } + + onUpdatePackagesChanged: { + cache.cachedUpdatePackagesJson = JSON.stringify(_clonePackageList(updatePackages)); + } + + onLastSyncChanged: { + cache.cachedLastSync = lastSync; + } +} From 55fd6361c5605d0459bb75e47070cc04aee3bc4e Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 00:58:41 +0300 Subject: [PATCH 25/54] Remove unnecessary tooltip for non-Arch users in ArchUpdater widget since we now gate the toggle itself to be visiable only in archbased --- Modules/Bar/Widgets/ArchUpdater.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 66378ff..5a2e359 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -15,8 +15,6 @@ NIconButton { icon: !ArchUpdaterService.ready ? "block" : (ArchUpdaterService.busy ? "sync" : (ArchUpdaterService.updatePackages.length > 0 ? "system_update" : "task_alt")) tooltipText: { - if (!ArchUpdaterService.isArchBased) - return "Arch users already ran 'sudo pacman -Syu' for breakfast."; if (!ArchUpdaterService.checkupdatesAvailable) return "Please install pacman-contrib to use this feature."; if (ArchUpdaterService.busy) From 1d625ac09887f60f44c7db8cb2167731267216e4 Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 01:03:04 +0300 Subject: [PATCH 26/54] opsies haha --- Modules/SettingsPanel/Tabs/BarTab.qml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 214408e..005a93f 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -128,17 +128,6 @@ ColumnLayout { } } - NToggle { - visible: ArchUpdaterService.isArchBased - label: "Show Arch Updater" - description: "Show the Arch Linux updates widget." - checked: Settings.data.bar.showArchUpdater - onToggled: checked => { - Settings.data.bar.showArchUpdater = checked - } - } - - NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL * scaling From f85bdfead97d165952a0e588c7c6ed51ebfff1c8 Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 01:24:44 +0300 Subject: [PATCH 27/54] restore the fun easter egg when not archbased since it can be enabled when it's not --- Commons/Settings.qml | 1 - Modules/Bar/Widgets/ArchUpdater.qml | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 3600c78..074ee60 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -123,7 +123,6 @@ Singleton { property bool showActiveWindowIcon: true property bool alwaysShowBatteryPercentage: false property real backgroundOpacity: 1.0 - property bool showArchUpdater: true property list monitors: [] // Widget configuration for modular bar system diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 5a2e359..4fd397d 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -4,7 +4,6 @@ import qs.Widgets NIconButton { id: root - visible: Settings.data.bar.showArchUpdater && ArchUpdaterService.isArchBased sizeMultiplier: 0.8 colorBg: Color.mSurfaceVariant @@ -15,6 +14,8 @@ NIconButton { icon: !ArchUpdaterService.ready ? "block" : (ArchUpdaterService.busy ? "sync" : (ArchUpdaterService.updatePackages.length > 0 ? "system_update" : "task_alt")) tooltipText: { + if (!ArchUpdaterService.isArchBased) + return "Arch users already ran 'sudo pacman -Syu' for breakfast."; if (!ArchUpdaterService.checkupdatesAvailable) return "Please install pacman-contrib to use this feature."; if (ArchUpdaterService.busy) From 348b6edbc9655640b2d90604967bbbd71bf4efbd Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 01:28:39 +0300 Subject: [PATCH 28/54] remove spaces to revert original state --- Modules/SettingsPanel/Tabs/BarTab.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 005a93f..906d958 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -127,7 +127,7 @@ ColumnLayout { Settings.data.bar.alwaysShowBatteryPercentage = checked } } - + NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginL * scaling From 9209c5d526c98bc2ea770eecff104cbbdb3a705e Mon Sep 17 00:00:00 2001 From: Lysec <52084453+Ly-sec@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:51:22 +0200 Subject: [PATCH 29/54] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d373d02..b9acd5d 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ While I actually didn't want to accept donations, more and more people are askin Thank you to everyone who supports me and this project 💜! * Gohma +* [PikaOS](https://pika-os.com/) --- From 5a90d280030a883774b926d17af306eb5e06634a Mon Sep 17 00:00:00 2001 From: Lysec <52084453+Ly-sec@users.noreply.github.com> Date: Sun, 24 Aug 2025 01:52:35 +0200 Subject: [PATCH 30/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9acd5d..8f5130b 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ While I actually didn't want to accept donations, more and more people are askin Thank you to everyone who supports me and this project 💜! * Gohma -* [PikaOS](https://pika-os.com/) +* PikaOS --- From f8f1e789d4eef60c62c843aa17adbf9619d2660d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 11:12:45 +0200 Subject: [PATCH 31/54] Fix WiFi, Bluetooth and Battery symbol visibility states --- Modules/Bar/Bar.qml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index ae0ad9f..43b9a5a 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland +import Quickshell.Services.UPower import qs.Commons import qs.Services import qs.Widgets @@ -63,6 +64,12 @@ Variants { id: leftWidgetLoader sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true + visible: { + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false + if (modelData === "Battery" && !shouldShowBattery()) return false + return true + } anchors.verticalCenter: parent.verticalCenter onStatusChanged: { if (status === Loader.Error) { @@ -90,6 +97,12 @@ Variants { id: centerWidgetLoader sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true + visible: { + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false + if (modelData === "Battery" && !shouldShowBattery()) return false + return true + } anchors.verticalCenter: parent.verticalCenter onStatusChanged: { if (status === Loader.Error) { @@ -118,6 +131,11 @@ Variants { id: rightWidgetLoader sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true + visible: { + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false + return true + } anchors.verticalCenter: parent.verticalCenter onStatusChanged: { if (status === Loader.Error) { @@ -131,6 +149,13 @@ Variants { } } + // Helper function to check if battery widget should be visible (same logic as Battery.qml) + function shouldShowBattery() { + // For now, always show battery widget and let it handle its own visibility + // The Battery widget has its own testMode and visibility logic + return true + } + // Widget loader instance WidgetLoader { id: widgetLoader From 1f9dec3e4c23f012bdd5fe269b3e50d52d58853e Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 12:18:36 +0200 Subject: [PATCH 32/54] Add clipboard ipc call, edit README --- Modules/IPC/IPCManager.qml | 8 +++++++- README.md | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index e8ac25c..93a1add 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -33,13 +33,17 @@ Item { } } - // For backward compatibility, should be removed soon(tmc) IpcHandler { target: "appLauncher" function toggle() { launcherPanel.toggle(Quickshell.screens[0]) } + + function clipboard() { + launcherPanel.toggle(Quickshell.screens[0]) + launcherPanel.searchText = ">clip " + } } IpcHandler { @@ -81,4 +85,6 @@ Item { powerPanel.toggle(Quickshell.screens[0]) } } + + } diff --git a/README.md b/README.md index 8f5130b..7895371 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,25 @@ mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctal # Start the shell qs -# Toggle launcher +# Launcher qs ipc call appLauncher toggle +# Clipboard History +qs ipc call appLauncher clipboard + +# Brightness +qs ipc call brightness increase +qs ipc call brightness decrease + +# Power Panel +qs ipc call powerPanel toggle + +# Idle Inhibitor +qs ipc call idleInhibitor toggle + +# Settings Window +qs ipc call settings toggle + # Toggle lock screen qs ipc call lockScreen toggle ``` From c3515ddcc0254d35d80535b7765e7e17e265b534 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 12:58:40 +0200 Subject: [PATCH 33/54] Rename appLauncher IPC to launcher, add calculator IPC --- Modules/IPC/IPCManager.qml | 60 +++++++++++++++++++++-------------- Modules/Launcher/Launcher.qml | 39 ++++++++++++++++------- README.md | 7 ++-- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 93a1add..bcd1e3e 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -5,58 +5,75 @@ import qs.Services Item { id: root - + IpcHandler { target: "settings" - function toggle() { settingsPanel.toggle(Quickshell.screens[0]) } } - + IpcHandler { target: "notifications" - function toggleHistory() { notificationHistoryPanel.toggle(Quickshell.screens[0]) } - - function toggleDoNotDisturb() {// TODO + function toggleDoNotDisturb() { + // TODO } } - + IpcHandler { target: "idleInhibitor" - function toggle() { return IdleInhibitorService.manualToggle() } } - + IpcHandler { target: "appLauncher" - function toggle() { launcherPanel.toggle(Quickshell.screens[0]) } - function clipboard() { launcherPanel.toggle(Quickshell.screens[0]) - launcherPanel.searchText = ">clip " + // Use the setSearchText function to set clipboard mode + Qt.callLater(() => { + launcherPanel.setSearchText(">clip ") + }) + } + function calculator() { + launcherPanel.toggle(Quickshell.screens[0]) + // Use the setSearchText function to set calculator mode + Qt.callLater(() => { + launcherPanel.setSearchText(">calc ") + }) } } - + IpcHandler { target: "launcher" - function toggle() { launcherPanel.toggle(Quickshell.screens[0]) } + function clipboard() { + launcherPanel.toggle(Quickshell.screens[0]) + // Use the setSearchText function to set clipboard mode + Qt.callLater(() => { + launcherPanel.setSearchText(">clip ") + }) + } + function calculator() { + launcherPanel.toggle(Quickshell.screens[0]) + // Use the setSearchText function to set calculator mode + Qt.callLater(() => { + launcherPanel.setSearchText(">calc ") + }) + } } - + IpcHandler { target: "lockScreen" - function toggle() { // Only lock if not already locked (prevents the red screen issue) // Note: No unlock via IPC for security reasons @@ -65,26 +82,21 @@ Item { } } } - + IpcHandler { target: "brightness" - function increase() { BrightnessService.increaseBrightness() } - function decrease() { BrightnessService.decreaseBrightness() } } - + IpcHandler { target: "powerPanel" - function toggle() { powerPanel.toggle(Quickshell.screens[0]) } } - - -} +} \ No newline at end of file diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 4679200..f90963c 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -24,10 +24,28 @@ NPanel { panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left")) panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right")) + // Properties + property string searchText: "" + + // Add function to set search text programmatically + function setSearchText(text) { + searchText = text + if (searchInput) { + searchInput.text = text + searchInput.cursorPosition = text.length + searchInput.forceActiveFocus() + } + } + onOpened: { // Reset state when panel opens to avoid sticky modes - searchText = "" - selectedIndex = 0 + if (searchText === "") { + searchText = "" + selectedIndex = 0 + } + if (searchInput) { + searchInput.forceActiveFocus() + } } // Import modular components @@ -50,7 +68,6 @@ NPanel { // Properties property var desktopEntries: DesktopEntries.applications.values - property string searchText: "" property int selectedIndex: 0 // Refresh clipboard when user starts typing clipboard commands @@ -141,15 +158,11 @@ NPanel { // Command execution functions function executeCalcCommand() { - searchText = ">calc " - searchInput.text = searchText - searchInput.cursorPosition = searchText.length + setSearchText(">calc ") } function executeClipCommand() { - searchText = ">clip " - searchInput.text = searchText - searchInput.cursorPosition = searchText.length + setSearchText(">clip ") } // Navigation functions @@ -252,8 +265,12 @@ NPanel { anchors.leftMargin: Style.marginS * scaling anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + text: searchText onTextChanged: { - searchText = text + // Update the parent searchText property + if (searchText !== text) { + searchText = text + } // Defer selectedIndex reset to avoid binding loops Qt.callLater(() => selectedIndex = 0) } @@ -504,4 +521,4 @@ NPanel { } } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 7895371..019fcf0 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,13 @@ mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctal qs # Launcher -qs ipc call appLauncher toggle +qs ipc call launcher toggle # Clipboard History -qs ipc call appLauncher clipboard +qs ipc call launcher clipboard + +# Calculator +qs ipc call launcher calculator # Brightness qs ipc call brightness increase From a7b7c03877851a50e6f97c68b9d1c599c5daeaed Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 08:48:39 -0400 Subject: [PATCH 34/54] Autoformatting + converted a bunch console.log to logger, hiding some extra logging --- Commons/Settings.qml | 431 +++++++++++++------------ Modules/Bar/Bar.qml | 24 +- Modules/Bar/Widgets/ActiveWindow.qml | 4 +- Modules/Bar/Widgets/KeyboardLayout.qml | 3 +- Modules/IPC/IPCManager.qml | 37 ++- Modules/Launcher/Calculator.qml | 2 +- Modules/Launcher/Launcher.qml | 4 +- Modules/LockScreen/LockScreen.qml | 3 +- Modules/SettingsPanel/Tabs/BarTab.qml | 29 +- Services/CompositorService.qml | 8 +- Widgets/NWidgetCard.qml | 125 +++---- 11 files changed, 341 insertions(+), 329 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 02554f3..771cbf9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -1,4 +1,5 @@ pragma Singleton + import QtQuick import Quickshell import Quickshell.Io @@ -6,243 +7,249 @@ import qs.Commons import qs.Services Singleton { - id: root + id: root - // Define our app directories - // Default config directory: ~/.config/noctalia - // Default cache directory: ~/.cache/noctalia - property string shellName: "noctalia" - property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" - property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/" - property string cacheDirImages: cacheDir + "images/" + // Define our app directories + // Default config directory: ~/.config/noctalia + // Default cache directory: ~/.cache/noctalia + property string shellName: "noctalia" + property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") + || Quickshell.env( + "HOME") + "/.config") + "/" + shellName + "/" + property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env( + "HOME") + "/.cache") + "/" + shellName + "/" + property string cacheDirImages: cacheDir + "images/" - property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") - property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") - property string defaultAvatar: Quickshell.env("HOME") + "/.face" + property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png") + property string defaultAvatar: Quickshell.env("HOME") + "/.face" - // Used to access via Settings.data.xxx.yyy - property alias data: adapter + // Used to access via Settings.data.xxx.yyy + property alias data: adapter - // Flag to prevent unnecessary wallpaper calls during reloads - property bool isInitialLoad: true + // Flag to prevent unnecessary wallpaper calls during reloads + property bool isInitialLoad: true - // Function to validate monitor configurations - function validateMonitorConfigurations() { - var availableScreenNames = []; - for (var i = 0; i < Quickshell.screens.length; i++) { - availableScreenNames.push(Quickshell.screens[i].name); - } - - Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]"); - Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]"); - - // Check bar monitors - if (adapter.bar.monitors.length > 0) { - var hasValidBarMonitor = false; - for (var j = 0; j < adapter.bar.monitors.length; j++) { - if (availableScreenNames.includes(adapter.bar.monitors[j])) { - hasValidBarMonitor = true; - break; - } - } - if (!hasValidBarMonitor) { - Logger.log("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens"); - adapter.bar.monitors = []; - } else { - Logger.log("Settings", "Found valid bar monitors, keeping configuration"); - } - } else { - Logger.log("Settings", "Bar monitor list is empty, will show on all available screens"); - } - } - Item { - Component.onCompleted: { - - // ensure settings dir exists - Quickshell.execDetached(["mkdir", "-p", configDir]); - Quickshell.execDetached(["mkdir", "-p", cacheDir]); - Quickshell.execDetached(["mkdir", "-p", cacheDirImages]); - } + // Function to validate monitor configurations + function validateMonitorConfigurations() { + var availableScreenNames = [] + for (var i = 0; i < Quickshell.screens.length; i++) { + availableScreenNames.push(Quickshell.screens[i].name) } - // Don't write settings to disk immediately - // This avoid excessive IO when a variable changes rapidly (ex: sliders) - Timer { - id: saveTimer - running: false - interval: 1000 - onTriggered: settingsFileView.writeAdapter() + Logger.log("Settings", "Available monitors: [" + availableScreenNames.join(", ") + "]") + Logger.log("Settings", "Configured bar monitors: [" + adapter.bar.monitors.join(", ") + "]") + + // Check bar monitors + if (adapter.bar.monitors.length > 0) { + var hasValidBarMonitor = false + for (var j = 0; j < adapter.bar.monitors.length; j++) { + if (availableScreenNames.includes(adapter.bar.monitors[j])) { + hasValidBarMonitor = true + break + } + } + if (!hasValidBarMonitor) { + Logger.log("Settings", + "No configured bar monitors found on system, clearing bar monitor list to show on all screens") + adapter.bar.monitors = [] + } else { + Logger.log("Settings", "Found valid bar monitors, keeping configuration") + } + } else { + Logger.log("Settings", "Bar monitor list is empty, will show on all available screens") + } + } + Item { + Component.onCompleted: { + + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) + } + } + + // Don't write settings to disk immediately + // This avoid excessive IO when a variable changes rapidly (ex: sliders) + Timer { + id: saveTimer + running: false + interval: 1000 + onTriggered: settingsFileView.writeAdapter() + } + + FileView { + id: settingsFileView + path: settingsFile + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: saveTimer.start() + Component.onCompleted: function () { + reload() + } + onLoaded: function () { + Qt.callLater(function () { + if (isInitialLoad) { + Logger.log("Settings", "OnLoaded") + // Only set wallpaper on initial load, not on reloads + if (adapter.wallpaper.current !== "") { + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) + } + + // Validate monitor configurations, only once + // if none of the configured monitors exist, clear the lists + validateMonitorConfigurations() + } + + isInitialLoad = false + }) + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) + // File doesn't exist, create it with default values + writeAdapter() } - FileView { - id: settingsFileView - path: settingsFile - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: saveTimer.start() - Component.onCompleted: function () { - reload(); + JsonAdapter { + id: adapter + + // bar + property JsonObject bar: JsonObject { + property string position: "top" // Possible values: "top", "bottom" + property bool showActiveWindowIcon: true + property bool alwaysShowBatteryPercentage: false + property real backgroundOpacity: 1.0 + property list monitors: [] + + // Widget configuration for modular bar system + property JsonObject widgets + widgets: JsonObject { + property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] + property list center: ["Workspace"] + property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] } - onLoaded: function () { - Qt.callLater(function () { - if (isInitialLoad) { - Logger.log("Settings", "OnLoaded"); - // Only set wallpaper on initial load, not on reloads - if (adapter.wallpaper.current !== "") { - Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current); - WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true); - } + } - // Validate monitor configurations, only once - // if none of the configured monitors exist, clear the lists - validateMonitorConfigurations(); - } + // general + property JsonObject general: JsonObject { + property string avatarImage: defaultAvatar + property bool dimDesktop: false + property bool showScreenCorners: false + property real radiusRatio: 1.0 + } - isInitialLoad = false; - }); - } - onLoadFailed: function (error) { - if (error.toString().includes("No such file") || error === 2) - // File doesn't exist, create it with default values - writeAdapter(); + // location + property JsonObject location: JsonObject { + property string name: "Tokyo" + property bool useFahrenheit: false + property bool reverseDayMonth: false + property bool use12HourClock: false + property bool showDateWithClock: false + } + + // screen recorder + property JsonObject screenRecorder: JsonObject { + property string directory: "~/Videos" + property int frameRate: 60 + property string audioCodec: "opus" + property string videoCodec: "h264" + property string quality: "very_high" + property string colorRange: "limited" + property bool showCursor: true + property string audioSource: "default_output" + property string videoSource: "portal" + } + + // wallpaper + property JsonObject wallpaper: JsonObject { + property string directory: "/usr/share/wallpapers" + property string current: "" + property bool isRandom: false + property int randomInterval: 300 + property JsonObject swww + + onDirectoryChanged: WallpaperService.listWallpapers() + onIsRandomChanged: WallpaperService.toggleRandomWallpaper() + onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() + + swww: JsonObject { + property bool enabled: false + property string resizeMethod: "crop" + property int transitionFps: 60 + property string transitionType: "random" + property real transitionDuration: 1.1 } + } - JsonAdapter { - id: adapter + // applauncher + property JsonObject appLauncher: JsonObject { + // When disabled, Launcher hides clipboard command and ignores cliphist + property bool enableClipboardHistory: true + // Position: center, top_left, top_right, bottom_left, bottom_right + property string position: "center" + property list pinnedExecs: [] + } - // bar - property JsonObject bar: JsonObject { - property string position: "top" // Possible values: "top", "bottom" - property bool showActiveWindowIcon: true - property bool alwaysShowBatteryPercentage: false - property real backgroundOpacity: 1.0 - property list monitors: [] + // dock + property JsonObject dock: JsonObject { + property bool autoHide: false + property bool exclusive: false + property list monitors: [] + } - // Widget configuration for modular bar system - property JsonObject widgets - widgets: JsonObject { - property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] - property list center: ["Workspace"] - property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] - } - } + // network + property JsonObject network: JsonObject { + property bool wifiEnabled: true + property bool bluetoothEnabled: true + } - // general - property JsonObject general: JsonObject { - property string avatarImage: defaultAvatar - property bool dimDesktop: false - property bool showScreenCorners: false - property real radiusRatio: 1.0 - } + // notifications + property JsonObject notifications: JsonObject { + property list monitors: [] + } - // location - property JsonObject location: JsonObject { - property string name: "Tokyo" - property bool useFahrenheit: false - property bool reverseDayMonth: false - property bool use12HourClock: false - property bool showDateWithClock: false - } + // audio + property JsonObject audio: JsonObject { + property bool showMiniplayerAlbumArt: false + property bool showMiniplayerCava: false + property string visualizerType: "linear" + property int volumeStep: 5 + property int cavaFrameRate: 60 + } - // screen recorder - property JsonObject screenRecorder: JsonObject { - property string directory: "~/Videos" - property int frameRate: 60 - property string audioCodec: "opus" - property string videoCodec: "h264" - property string quality: "very_high" - property string colorRange: "limited" - property bool showCursor: true - property string audioSource: "default_output" - property string videoSource: "portal" - } + // ui + property JsonObject ui: JsonObject { + property string fontDefault: "Roboto" // Default font for all text + property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal + property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays - // wallpaper - property JsonObject wallpaper: JsonObject { - property string directory: "/usr/share/wallpapers" - property string current: "" - property bool isRandom: false - property int randomInterval: 300 - property JsonObject swww + // Legacy compatibility + property string fontFamily: fontDefault // Keep for backward compatibility - onDirectoryChanged: WallpaperService.listWallpapers() - onIsRandomChanged: WallpaperService.toggleRandomWallpaper() - onRandomIntervalChanged: WallpaperService.restartRandomWallpaperTimer() + // Idle inhibitor state + property bool idleInhibitorEnabled: false + } - swww: JsonObject { - property bool enabled: false - property string resizeMethod: "crop" - property int transitionFps: 60 - property string transitionType: "random" - property real transitionDuration: 1.1 - } - } + // Scaling (not stored inside JsonObject, or it crashes) + property var monitorsScaling: { - // applauncher - property JsonObject appLauncher: JsonObject { - // When disabled, Launcher hides clipboard command and ignores cliphist - property bool enableClipboardHistory: true - // Position: center, top_left, top_right, bottom_left, bottom_right - property string position: "center" - property list pinnedExecs: [] - } + } - // dock - property JsonObject dock: JsonObject { - property bool autoHide: false - property bool exclusive: false - property list monitors: [] - } + // brightness + property JsonObject brightness: JsonObject { + property int brightnessStep: 5 + } - // network - property JsonObject network: JsonObject { - property bool wifiEnabled: true - property bool bluetoothEnabled: true - } - - // notifications - property JsonObject notifications: JsonObject { - property list monitors: [] - } - - // audio - property JsonObject audio: JsonObject { - property bool showMiniplayerAlbumArt: false - property bool showMiniplayerCava: false - property string visualizerType: "linear" - property int volumeStep: 5 - property int cavaFrameRate: 60 - } - - // ui - property JsonObject ui: JsonObject { - property string fontDefault: "Roboto" // Default font for all text - property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal - property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays - - // Legacy compatibility - property string fontFamily: fontDefault // Keep for backward compatibility - - // Idle inhibitor state - property bool idleInhibitorEnabled: false - } - - // Scaling (not stored inside JsonObject, or it crashes) - property var monitorsScaling: {} - - // brightness - property JsonObject brightness: JsonObject { - property int brightnessStep: 5 - } - - property JsonObject colorSchemes: JsonObject { - property bool useWallpaperColors: false - property string predefinedScheme: "" - property bool darkMode: true - // External app theming (GTK & Qt) - property bool themeApps: false - } - } + property JsonObject colorSchemes: JsonObject { + property bool useWallpaperColors: false + property string predefinedScheme: "" + property bool darkMode: true + // External app theming (GTK & Qt) + property bool themeApps: false + } } + } } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 43b9a5a..48ed76e 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -65,9 +65,12 @@ Variants { sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false - if (modelData === "Battery" && !shouldShowBattery()) return false + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) + return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) + return false + if (modelData === "Battery" && !shouldShowBattery()) + return false return true } anchors.verticalCenter: parent.verticalCenter @@ -98,9 +101,12 @@ Variants { sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false - if (modelData === "Battery" && !shouldShowBattery()) return false + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) + return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) + return false + if (modelData === "Battery" && !shouldShowBattery()) + return false return true } anchors.verticalCenter: parent.verticalCenter @@ -132,8 +138,10 @@ Variants { sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) return false + if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) + return false + if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) + return false return true } anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 990f532..904b5b4 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -7,7 +7,6 @@ import qs.Commons import qs.Services import qs.Widgets - Row { id: root anchors.verticalCenter: parent.verticalCenter @@ -43,8 +42,7 @@ Row { function getTitle() { // Use the service's focusedWindowTitle property which is updated immediately // when WindowOpenedOrChanged events are received - return CompositorService.focusedWindowTitle !== "(No active window)" ? - CompositorService.focusedWindowTitle : "" + return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } function getAppIcon() { diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index fd1e4fb..1ae3acb 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -25,10 +25,9 @@ Item { tooltipText: "Keyboard Layout: " + currentLayout onClicked: { + // You could open keyboard settings here if needed // For now, just show the current layout } } - - } diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index bcd1e3e..ad63934 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -5,31 +5,30 @@ import qs.Services Item { id: root - + IpcHandler { target: "settings" function toggle() { settingsPanel.toggle(Quickshell.screens[0]) } } - + IpcHandler { target: "notifications" function toggleHistory() { notificationHistoryPanel.toggle(Quickshell.screens[0]) } - function toggleDoNotDisturb() { - // TODO + function toggleDoNotDisturb() {// TODO } } - + IpcHandler { target: "idleInhibitor" function toggle() { return IdleInhibitorService.manualToggle() } } - + IpcHandler { target: "appLauncher" function toggle() { @@ -39,18 +38,18 @@ Item { launcherPanel.toggle(Quickshell.screens[0]) // Use the setSearchText function to set clipboard mode Qt.callLater(() => { - launcherPanel.setSearchText(">clip ") - }) + launcherPanel.setSearchText(">clip ") + }) } function calculator() { launcherPanel.toggle(Quickshell.screens[0]) // Use the setSearchText function to set calculator mode Qt.callLater(() => { - launcherPanel.setSearchText(">calc ") - }) + launcherPanel.setSearchText(">calc ") + }) } } - + IpcHandler { target: "launcher" function toggle() { @@ -60,18 +59,18 @@ Item { launcherPanel.toggle(Quickshell.screens[0]) // Use the setSearchText function to set clipboard mode Qt.callLater(() => { - launcherPanel.setSearchText(">clip ") - }) + launcherPanel.setSearchText(">clip ") + }) } function calculator() { launcherPanel.toggle(Quickshell.screens[0]) // Use the setSearchText function to set calculator mode Qt.callLater(() => { - launcherPanel.setSearchText(">calc ") - }) + launcherPanel.setSearchText(">calc ") + }) } } - + IpcHandler { target: "lockScreen" function toggle() { @@ -82,7 +81,7 @@ Item { } } } - + IpcHandler { target: "brightness" function increase() { @@ -92,11 +91,11 @@ Item { BrightnessService.decreaseBrightness() } } - + IpcHandler { target: "powerPanel" function toggle() { powerPanel.toggle(Quickshell.screens[0]) } } -} \ No newline at end of file +} diff --git a/Modules/Launcher/Calculator.qml b/Modules/Launcher/Calculator.qml index 8dae5bd..1082b89 100644 --- a/Modules/Launcher/Calculator.qml +++ b/Modules/Launcher/Calculator.qml @@ -33,7 +33,7 @@ QtObject { } } else { // Fallback to basic evaluation - console.log("AdvancedMath not available, using basic eval") + Logger.warn("Calculator", "AdvancedMath not available, using basic eval") // Basic preprocessing for common functions var processed = expression.trim( diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index f90963c..9bedecf 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -26,7 +26,7 @@ NPanel { // Properties property string searchText: "" - + // Add function to set search text programmatically function setSearchText(text) { searchText = text @@ -521,4 +521,4 @@ NPanel { } } } -} \ No newline at end of file +} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index f5ded6e..db7686f 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -167,7 +167,8 @@ Loader { Item { id: keyboardLayout - property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" + property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' + && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" } // Wallpaper image diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 906d958..40d59da 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -202,13 +202,13 @@ ColumnLayout { // Helper functions function addWidgetToSection(widgetName, section) { - console.log("Adding widget", widgetName, "to section", section) + //Logger.log("BarTab", "Adding widget", widgetName, "to section", section) var sectionArray = Settings.data.bar.widgets[section] if (sectionArray) { // Create a new array to avoid modifying the original var newArray = sectionArray.slice() newArray.push(widgetName) - console.log("Widget added. New array:", JSON.stringify(newArray)) + //Logger.log("BarTab", "Widget added. New array:", JSON.stringify(newArray)) // Assign the new array Settings.data.bar.widgets[section] = newArray @@ -216,34 +216,35 @@ ColumnLayout { } function removeWidgetFromSection(section, index) { - console.log("Removing widget from section", section, "at index", index) + // Logger.log("BarTab", "Removing widget from section", section, "at index", index) var sectionArray = Settings.data.bar.widgets[section] - console.log("Current section array:", JSON.stringify(sectionArray)) - + //Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray)) + if (sectionArray && index >= 0 && index < sectionArray.length) { // Create a new array to avoid modifying the original var newArray = sectionArray.slice() newArray.splice(index, 1) - console.log("Widget removed. New array:", JSON.stringify(newArray)) + //Logger.log("BarTab", "Widget removed. New array:", JSON.stringify(newArray)) // Assign the new array Settings.data.bar.widgets[section] = newArray - + // Force a settings save - console.log("Settings updated, triggering save...") - + //Logger.log("BarTab", "Settings updated, triggering save...") + // Verify the change was applied - Qt.setTimeout(function() { + Qt.setTimeout(function () { var updatedArray = Settings.data.bar.widgets[section] - console.log("Verification - updated section array:", JSON.stringify(updatedArray)) + //Logger.log("BarTab", "Verification - updated section array:", JSON.stringify(updatedArray)) }, 100) } else { - console.log("Invalid section or index:", section, index, "array length:", sectionArray ? sectionArray.length : "null") + //Logger.log("BarTab", "Invalid section or index:", section, index, "array length:", + // sectionArray ? sectionArray.length : "null") } } function reorderWidgetInSection(section, fromIndex, toIndex) { - console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex) + //Logger.log("BarTab", "Reordering widget in section", section, "from", fromIndex, "to", toIndex) var sectionArray = Settings.data.bar.widgets[section] if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0 && toIndex < sectionArray.length) { @@ -253,7 +254,7 @@ ColumnLayout { var item = newArray[fromIndex] newArray.splice(fromIndex, 1) newArray.splice(toIndex, 0, item) - console.log("Widget reordered. New array:", JSON.stringify(newArray)) + Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray)) // Assign the new array Settings.data.bar.widgets[section] = newArray diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index aaddde4..a1ec62b 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -298,8 +298,6 @@ Singleton { "isFocused": windowData.is_focused === true } - - if (existingIndex >= 0) { // Update existing window windows[existingIndex] = newWindow @@ -314,13 +312,13 @@ Singleton { const oldFocusedIndex = focusedWindowIndex focusedWindowIndex = windows.findIndex(w => w.id === windowData.id) updateFocusedWindowTitle() - + // Only emit activeWindowChanged if the focused window actually changed if (oldFocusedIndex !== focusedWindowIndex) { activeWindowChanged() } } else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) { - // If this is the currently focused window (but not newly focused), + // If this is the currently focused window (but not newly focused), // still update the title in case it changed, but don't emit activeWindowChanged updateFocusedWindowTitle() } @@ -467,7 +465,7 @@ Singleton { } else { focusedWindowTitle = "(No active window)" } - + // Emit signal if title actually changed if (oldTitle !== focusedWindowTitle) { windowTitleChanged() diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml index 22f4f5a..abf53c7 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NWidgetCard.qml @@ -114,7 +114,7 @@ NBox { id: widgetItem required property int index required property string modelData - + width: widgetContent.implicitWidth + 16 * scaling height: 40 * scaling radius: Style.radiusL * scaling @@ -127,7 +127,7 @@ NBox { Drag.active: mouseArea.drag.active Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 - + // Store the widget index for drag operations property int widgetIndex: index @@ -173,40 +173,40 @@ NBox { id: mouseArea anchors.fill: parent drag.target: parent - + onPressed: { // Check if the click is on the close button area const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling const closeButtonY = widgetContent.y const closeButtonWidth = 20 * scaling const closeButtonHeight = 20 * scaling - - if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth && - mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) { + + if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth && mouseY >= closeButtonY + && mouseY <= closeButtonY + closeButtonHeight) { // Click is on the close button, don't start drag mouse.accepted = false return } - + Logger.log("NWidgetCard", `Started dragging widget: ${modelData} at index ${index}`) // Bring to front when starting drag widgetItem.z = 1000 } - + onReleased: { Logger.log("NWidgetCard", `Released widget: ${modelData} at index ${index}`) // Reset z-index when drag ends widgetItem.z = 0 - + // Get the global mouse position const globalDropX = mouseArea.mouseX + widgetItem.x + widgetFlow.x const globalDropY = mouseArea.mouseY + widgetItem.y + widgetFlow.y - + // Find which widget the drop position is closest to let targetIndex = -1 let minDistance = Infinity - - for (let i = 0; i < widgetModel.length; i++) { + + for (var i = 0; i < widgetModel.length; i++) { if (i !== index) { // Get the position of other widgets const otherWidget = widgetFlow.children[i] @@ -214,13 +214,11 @@ NBox { // Calculate the center of the other widget const otherCenterX = otherWidget.x + otherWidget.width / 2 + widgetFlow.x const otherCenterY = otherWidget.y + otherWidget.height / 2 + widgetFlow.y - + // Calculate distance to the center of this widget - const distance = Math.sqrt( - Math.pow(globalDropX - otherCenterX, 2) + - Math.pow(globalDropY - otherCenterY, 2) - ) - + const distance = Math.sqrt(Math.pow(globalDropX - otherCenterX, + 2) + Math.pow(globalDropY - otherCenterY, 2)) + if (distance < minDistance) { minDistance = distance targetIndex = otherWidget.widgetIndex @@ -228,12 +226,15 @@ NBox { } } } - + // Only reorder if we found a valid target and it's different from current position if (targetIndex !== -1 && targetIndex !== index) { const fromIndex = index const toIndex = targetIndex - Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed(2)})`) + Logger.log( + "NWidgetCard", + `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed( + 2)})`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } else { Logger.log("NWidgetCard", `No valid drop target found for widget at index ${index}`) @@ -244,29 +245,29 @@ NBox { } } - // Drop zone at the beginning (positioned absolutely) - DropArea { - id: startDropZone - width: 40 * scaling - height: 40 * scaling - x: widgetFlow.x - y: widgetFlow.y + (widgetFlow.height - height) / 2 - keys: ["widget"] - z: 1001 // Above the Flow - - Rectangle { - anchors.fill: parent - color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent - border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent - border.width: startDropZone.containsDrag ? 2 : 0 - radius: Style.radiusS * scaling - } - - onEntered: function(drag) { + // Drop zone at the beginning (positioned absolutely) + DropArea { + id: startDropZone + width: 40 * scaling + height: 40 * scaling + x: widgetFlow.x + y: widgetFlow.y + (widgetFlow.height - height) / 2 + keys: ["widget"] + z: 1001 // Above the Flow + + Rectangle { + anchors.fill: parent + color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent + border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent + border.width: startDropZone.containsDrag ? 2 : 0 + radius: Style.radiusS * scaling + } + + onEntered: function (drag) { Logger.log("NWidgetCard", "Entered start drop zone") } - - onDropped: function(drop) { + + onDropped: function (drop) { Logger.log("NWidgetCard", "Dropped on start zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex @@ -279,29 +280,29 @@ NBox { } } - // Drop zone at the end (positioned absolutely) - DropArea { - id: endDropZone - width: 40 * scaling - height: 40 * scaling - x: widgetFlow.x + widgetFlow.width - width - y: widgetFlow.y + (widgetFlow.height - height) / 2 - keys: ["widget"] - z: 1001 // Above the Flow - - Rectangle { - anchors.fill: parent - color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent - border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent - border.width: endDropZone.containsDrag ? 2 : 0 - radius: Style.radiusS * scaling - } - - onEntered: function(drag) { + // Drop zone at the end (positioned absolutely) + DropArea { + id: endDropZone + width: 40 * scaling + height: 40 * scaling + x: widgetFlow.x + widgetFlow.width - width + y: widgetFlow.y + (widgetFlow.height - height) / 2 + keys: ["widget"] + z: 1001 // Above the Flow + + Rectangle { + anchors.fill: parent + color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent + border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent + border.width: endDropZone.containsDrag ? 2 : 0 + radius: Style.radiusS * scaling + } + + onEntered: function (drag) { Logger.log("NWidgetCard", "Entered end drop zone") } - - onDropped: function(drop) { + + onDropped: function (drop) { Logger.log("NWidgetCard", "Dropped on end zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex From aedbacc495e184e06e56fb7bc4a4292e6f9f4e16 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 15:22:12 +0200 Subject: [PATCH 35/54] Reset Launcher text input on close --- Modules/Launcher/Launcher.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index f90963c..f0d9c11 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -48,6 +48,16 @@ NPanel { } } + onClosed: { + // Reset search bar when launcher is closed + searchText = "" + selectedIndex = 0 + if (searchInput) { + searchInput.text = "" + searchInput.cursorPosition = 0 + } + } + // Import modular components Calculator { id: calculator From c14e72ebbdf1096d1570a495bc06bb1355f20ed6 Mon Sep 17 00:00:00 2001 From: Anas Khalifa Date: Sun, 24 Aug 2025 16:29:19 +0300 Subject: [PATCH 36/54] more cleanup with forgotten things --- Services/ArchUpdaterService.qml | 73 ++++++++------------------------- 1 file changed, 17 insertions(+), 56 deletions(-) diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index dbe9459..ea52728 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -2,7 +2,6 @@ pragma Singleton import Quickshell import QtQuick import Quickshell.Io -import qs.Commons Singleton { id: updateService @@ -54,13 +53,6 @@ Singleton { Quickshell.execDetached(["notify-send", "-a", app, "-i", icon, String(title || ""), String(body || "")]); } - function startUpdateProcess(cmd) { - pkgProc.command = cmd; - pkgProc.running = true; - killTimer.interval = lastWasFull ? 60 * 1000 : minuteMs; - killTimer.restart(); - } - function doPoll(forceFull = false) { if (busy) return; @@ -91,61 +83,38 @@ Singleton { Process { id: pkgProc - onExited: function (exitCode, exitStatus) { + onExited: function () { + var exitCode = arguments[0]; killTimer.stop(); if (exitCode !== 0 && exitCode !== 2) { updateService.failureCount++; - Logger.warn("UpdateService", `checkupdates failed (code: ${exitCode}, status: ${exitStatus})`); + console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")"); if (updateService.failureCount >= updateService.failureThreshold) { updateService.notify(qsTr("Update check failed"), qsTr(`Exit code: ${exitCode} (failed ${updateService.failureCount} times)`)); updateService.failureCount = 0; } updateService.updatePackages = []; + return; } + + updateService.failureCount = 0; + const parsed = updateService._parseUpdateOutput(out.text); + updateService.updatePackages = parsed.pkgs; + + if (updateService.lastWasFull) { + updateService.lastSync = Date.now(); + } + + cache.cachedUpdatePackagesJson = JSON.stringify(updateService._clonePackageList(updateService.updatePackages)); + cache.cachedLastSync = updateService.lastSync; + updateService._summarizeAndNotify(); } stdout: StdioCollector { id: out - onStreamFinished: { - if (!pkgProc.running || updateService.busy) - return; - killTimer.stop(); - - const parsed = updateService._parseUpdateOutput(out.text); - updateService.updatePackages = parsed.pkgs; - - if (updateService.lastWasFull) { - updateService.lastSync = Date.now(); - } - - cache.cachedUpdatePackagesJson = JSON.stringify(updateService._clonePackageList(updateService.updatePackages)); - cache.cachedLastSync = updateService.lastSync; - updateService._summarizeAndNotify(parsed.pkgs, updateService.updates); - } - } - stderr: StdioCollector { - id: err - onStreamFinished: { - const stderrText = (err.text || "").trim(); - if (stderrText) { - Logger.warn("UpdateService", "stderr:", stderrText); - updateService.failureCount++; - updateService._notifyOnFailureThreshold(stderrText); - } else { - updateService.failureCount = 0; - } - } } } - function _notifyOnFailureThreshold(body) { - if (failureCount >= failureThreshold) { - notify(qsTr("Update check failed"), String(body || "")); - failureCount = 0; - return true; - } - return false; - } function _clonePackageList(list) { const src = Array.isArray(list) ? list : []; @@ -206,17 +175,9 @@ Singleton { repeat: false onTriggered: { if (pkgProc.running) { - Logger.error("UpdateService", "Update check killed (timeout)"); + console.error("[UpdateService] Update check killed (timeout)"); updateService.notify(qsTr("Update check killed"), qsTr("Process took too long")); } } } - - onUpdatePackagesChanged: { - cache.cachedUpdatePackagesJson = JSON.stringify(_clonePackageList(updatePackages)); - } - - onLastSyncChanged: { - cache.cachedLastSync = lastSync; - } } From 0b5e2e86f023680295bea9a03b342eb20b4a0674 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 10:02:00 -0400 Subject: [PATCH 37/54] Location/Weather: using a different API to geocode location name + fixed weather display which was offseted by a day --- Modules/SettingsPanel/Tabs/BarTab.qml | 5 +++-- Modules/SidePanel/Cards/WeatherCard.qml | 5 ++++- Services/LocationService.qml | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 40d59da..d0476c6 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -216,10 +216,10 @@ ColumnLayout { } function removeWidgetFromSection(section, index) { - // Logger.log("BarTab", "Removing widget from section", section, "at index", index) + // Logger.log("BarTab", "Removing widget from section", section, "at index", index) var sectionArray = Settings.data.bar.widgets[section] - //Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray)) + //Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray)) if (sectionArray && index >= 0 && index < sectionArray.length) { // Create a new array to avoid modifying the original var newArray = sectionArray.slice() @@ -238,6 +238,7 @@ ColumnLayout { //Logger.log("BarTab", "Verification - updated section array:", JSON.stringify(updatedArray)) }, 100) } else { + //Logger.log("BarTab", "Invalid section or index:", section, index, "array length:", // sectionArray ? sectionArray.length : "null") } diff --git a/Modules/SidePanel/Cards/WeatherCard.qml b/Modules/SidePanel/Cards/WeatherCard.qml index 468d12c..e9c7a17 100644 --- a/Modules/SidePanel/Cards/WeatherCard.qml +++ b/Modules/SidePanel/Cards/WeatherCard.qml @@ -90,7 +90,10 @@ NBox { Layout.alignment: Qt.AlignHCenter spacing: Style.marginS * scaling NText { - text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd") + text: { + var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")) + return Qt.formatDateTime(weatherDate, "ddd") + } color: Color.mOnSurface } NIcon { diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 0ac0ac2..c758856 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -109,8 +109,8 @@ Singleton { // -------------------------------- function _geocodeLocation(locationName, callback, errorCallback) { - Logger.log("Location", "Geocoding from api.open-meteo.com") - var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent( + Logger.log("Location", "Geocoding location name") + var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent( locationName) + "&language=en&format=json" var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { @@ -119,8 +119,8 @@ Singleton { try { var geoData = JSON.parse(xhr.responseText) // Logger.logJSON.stringify(geoData)) - if (geoData.results && geoData.results.length > 0) { - callback(geoData.results[0].latitude, geoData.results[0].longitude) + if (geoData.lat != null) { + callback(geoData.lat, geoData.lng) } else { errorCallback("Location", "could not resolve location name") } From ac1457a6c6e2cdc3defa045281c4b83c73400303 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 16:35:10 +0200 Subject: [PATCH 38/54] Add GUI for ArchUpdater --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 227 +++++++++++++++ Modules/Bar/Widgets/ArchUpdater.qml | 47 ++- Services/ArchUpdaterService.qml | 270 ++++++++---------- Services/PanelService.qml | 3 + shell.qml | 8 + 5 files changed, 393 insertions(+), 162 deletions(-) create mode 100644 Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml new file mode 100644 index 0000000..70a23a3 --- /dev/null +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -0,0 +1,227 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NPanel { + id: root + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: true + + // Auto-refresh when service updates + Connections { + target: ArchUpdaterService + function onUpdatePackagesChanged() { + // Force UI update when packages change + if (root.visible) { + // Small delay to ensure data is fully updated + Qt.callLater(() => { + // Force a UI update by triggering a property change + ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages; + }, 100); + } + } + } + + panelContent: Rectangle { + color: Color.mSurface + radius: Style.radiusL * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NIcon { + text: "system_update" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary + } + + Text { + text: "System Updates" + font.pointSize: Style.fontSizeL * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: root.close() + } + } + + NDivider { Layout.fillWidth: true } + + // Update summary + Text { + text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length !== 1 ? "s" : "") + " can be updated" + font.pointSize: Style.fontSizeL * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true + } + + // Package selection info + Text { + text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected" + font.pointSize: Style.fontSizeS * scaling + font.family: Settings.data.ui.fontDefault + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + } + + // Package list + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurfaceVariant + radius: Style.radiusM * scaling + + ListView { + id: packageListView + anchors.fill: parent + anchors.margins: Style.marginS * scaling + clip: true + model: ArchUpdaterService.updatePackages + spacing: Style.marginXS * scaling + + delegate: Rectangle { + width: packageListView.width + height: 50 * scaling + color: Color.transparent + radius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling + + // Checkbox for selection + NIconButton { + id: checkbox + icon: "check_box_outline_blank" + onClicked: { + const isSelected = ArchUpdaterService.isPackageSelected(modelData.name); + if (isSelected) { + ArchUpdaterService.togglePackageSelection(modelData.name); + icon = "check_box_outline_blank"; + colorFg = Color.mOnSurfaceVariant; + } else { + ArchUpdaterService.togglePackageSelection(modelData.name); + icon = "check_box"; + colorFg = Color.mPrimary; + } + } + colorBg: Color.transparent + colorFg: Color.mOnSurfaceVariant + Layout.preferredWidth: 30 * scaling + Layout.preferredHeight: 30 * scaling + + Component.onCompleted: { + // Set initial state + if (ArchUpdaterService.isPackageSelected(modelData.name)) { + icon = "check_box"; + colorFg = Color.mPrimary; + } + } + } + + // Package info + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS * scaling + + Text { + text: modelData.name + font.pointSize: Style.fontSizeM * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true + } + + Text { + text: modelData.oldVersion + " → " + modelData.newVersion + font.pointSize: Style.fontSizeS * scaling + font.family: Settings.data.ui.fontDefault + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + } + } + } + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + } + } + + + + // Action buttons + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NIconButton { + icon: "refresh" + tooltipText: "Check for updates" + onClicked: { + ArchUpdaterService.doPoll(); + } + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + + NIconButton { + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" + enabled: !ArchUpdaterService.updateInProgress + onClicked: { + ArchUpdaterService.runUpdate(); + } + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + + NIconButton { + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" + enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 + onClicked: { + if (ArchUpdaterService.selectedPackagesCount > 0) { + ArchUpdaterService.runSelectiveUpdate(); + } + } + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : + (ArchUpdaterService.selectedPackagesCount > 0 ? Color.mSecondary : Color.mSurfaceVariant) + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : + (ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant) + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + } + } + } +} diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 4fd397d..4881966 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -1,48 +1,71 @@ import qs.Commons import qs.Services import qs.Widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts NIconButton { id: root sizeMultiplier: 0.8 + readonly property real scaling: ScalingService.scale(screen) + colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - icon: !ArchUpdaterService.ready ? "block" : (ArchUpdaterService.busy ? "sync" : (ArchUpdaterService.updatePackages.length > 0 ? "system_update" : "task_alt")) + // Enhanced icon states with better visual feedback + icon: { + if (ArchUpdaterService.busy) return "sync" + if (ArchUpdaterService.updatePackages.length > 0) { + // Show different icons based on update count + const count = ArchUpdaterService.updatePackages.length + if (count > 50) return "system_update_alt" // Many updates + if (count > 10) return "system_update" // Moderate updates + return "system_update" // Few updates + } + return "task_alt" + } + // Enhanced tooltip with more information tooltipText: { - if (!ArchUpdaterService.isArchBased) - return "Arch users already ran 'sudo pacman -Syu' for breakfast."; - if (!ArchUpdaterService.checkupdatesAvailable) - return "Please install pacman-contrib to use this feature."; if (ArchUpdaterService.busy) return "Checking for updates…"; var count = ArchUpdaterService.updatePackages.length; if (count === 0) - return "No updates available"; + return "System is up to date ✓"; var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:"); var list = ArchUpdaterService.updatePackages || []; var s = ""; - var limit = Math.min(list.length, 10); + var limit = Math.min(list.length, 8); // Reduced to 8 for better readability for (var i = 0; i < limit; ++i) { var p = list[i]; s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion); } - if (list.length > 10) - s += "\n… and " + (list.length - 10) + " more"; + if (list.length > 8) + s += "\n… and " + (list.length - 8) + " more"; - return header + "\n" + s; + return header + "\n\n" + s + "\n\nClick to update system"; } + // Enhanced click behavior with confirmation onClicked: { - if (!ArchUpdaterService.ready || ArchUpdaterService.busy) + if (ArchUpdaterService.busy) return; - ArchUpdaterService.runUpdate(); + + if (ArchUpdaterService.updatePackages.length > 0) { + // Show confirmation dialog for updates + PanelService.updatePanel.toggle(screen); + } else { + // Just refresh if no updates available + ArchUpdaterService.doPoll(); + } } + + } diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index ea52728..ed392ef 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -5,179 +5,149 @@ import Quickshell.Io Singleton { id: updateService - property bool isArchBased: false - property bool checkupdatesAvailable: false - readonly property bool ready: isArchBased && checkupdatesAvailable - readonly property bool busy: pkgProc.running + + // Core properties + readonly property bool busy: checkupdatesProcess.running readonly property int updates: updatePackages.length property var updatePackages: [] - property double lastSync: 0 - property bool lastWasFull: false - property int failureCount: 0 - readonly property int failureThreshold: 5 - readonly property int quickTimeoutMs: 12 * 1000 - readonly property int minuteMs: 60 * 1000 - readonly property int pollInterval: 1 * minuteMs - readonly property int syncInterval: 15 * minuteMs - property int lastNotifiedUpdates: 0 - - property var updateCommand: ["xdg-terminal-exec", "--title=System Updates", "-e", "sh", "-c", "sudo pacman -Syu; printf '\n\nUpdate finished. Press Enter to exit...'; read _"] - - PersistentProperties { - id: cache - reloadableId: "ArchCheckerCache" - - property string cachedUpdatePackagesJson: "[]" - property double cachedLastSync: 0 - } - - Component.onCompleted: { - const persisted = JSON.parse(cache.cachedUpdatePackagesJson || "[]"); - if (persisted.length) - updatePackages = _clonePackageList(persisted); - if (cache.cachedLastSync > 0) - lastSync = cache.cachedLastSync; - } - - function runUpdate() { - if (updates > 0) { - Quickshell.execDetached(updateCommand); - } else { - doPoll(true); - } - } - - function notify(title, body) { - const app = "UpdateService"; - const icon = "system-software-update"; - Quickshell.execDetached(["notify-send", "-a", app, "-i", icon, String(title || ""), String(body || "")]); - } - - function doPoll(forceFull = false) { - if (busy) - return; - const full = forceFull || (Date.now() - lastSync > syncInterval); - lastWasFull = full; - - pkgProc.command = full ? ["checkupdates", "--nocolor"] : ["checkupdates", "--nosync", "--nocolor"]; - pkgProc.running = true; - killTimer.restart(); - } - + property var selectedPackages: [] + property int selectedPackagesCount: 0 + property bool updateInProgress: false + + // Process for checking updates Process { - id: pacmanCheck - running: true - command: ["sh", "-c", "p=$(command -v pacman >/dev/null && echo yes || echo no); c=$(command -v checkupdates >/dev/null && echo yes || echo no); echo \"$p $c\""] - stdout: StdioCollector { - onStreamFinished: { - const parts = (text || "").trim().split(/\s+/); - updateService.isArchBased = (parts[0] === "yes"); - updateService.checkupdatesAvailable = (parts[1] === "yes"); - if (updateService.ready) { - updateService.doPoll(); - pollTimer.start(); - } - } - } - } - - Process { - id: pkgProc - onExited: function () { - var exitCode = arguments[0]; - killTimer.stop(); + id: checkupdatesProcess + command: ["checkupdates"] + onExited: function(exitCode) { if (exitCode !== 0 && exitCode !== 2) { - updateService.failureCount++; console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")"); - if (updateService.failureCount >= updateService.failureThreshold) { - updateService.notify(qsTr("Update check failed"), qsTr(`Exit code: ${exitCode} (failed ${updateService.failureCount} times)`)); - updateService.failureCount = 0; - } - updateService.updatePackages = []; + updatePackages = []; return; } - - updateService.failureCount = 0; - const parsed = updateService._parseUpdateOutput(out.text); - updateService.updatePackages = parsed.pkgs; - - if (updateService.lastWasFull) { - updateService.lastSync = Date.now(); - } - - cache.cachedUpdatePackagesJson = JSON.stringify(updateService._clonePackageList(updateService.updatePackages)); - cache.cachedLastSync = updateService.lastSync; - updateService._summarizeAndNotify(); } - stdout: StdioCollector { - id: out + onStreamFinished: { + parseCheckupdatesOutput(text); + } } } - - - function _clonePackageList(list) { - const src = Array.isArray(list) ? list : []; - return src.map(p => ({ - name: String(p.name || ""), - oldVersion: String(p.oldVersion || ""), - newVersion: String(p.newVersion || "") - })); - } - - function _parseUpdateOutput(rawText) { - const raw = (rawText || "").trim(); - const lines = raw ? raw.split(/\r?\n/) : []; - const pkgs = []; - for (let i = 0; i < lines.length; ++i) { - const m = lines[i].match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/); + + // Parse checkupdates output + function parseCheckupdatesOutput(output) { + const lines = output.trim().split('\n').filter(line => line.trim()); + const packages = []; + + for (const line of lines) { + const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/); if (m) { - pkgs.push({ + packages.push({ name: m[1], oldVersion: m[2], - newVersion: m[3] + newVersion: m[3], + description: `${m[1]} ${m[2]} -> ${m[3]}` }); } } - return { - raw, - pkgs - }; + + updatePackages = packages; } - - function _summarizeAndNotify() { + + // Check for updates + function doPoll() { + if (busy) return; + checkupdatesProcess.running = true; + } + + // Update all packages + function runUpdate() { if (updates === 0) { - lastNotifiedUpdates = 0; + doPoll(); return; } - if (updates <= lastNotifiedUpdates) - return; - const added = updates - lastNotifiedUpdates; - const msg = added === 1 ? qsTr("One new package can be upgraded (") + updates + qsTr(")") : `${added} ${qsTr("new packages can be upgraded (")} ${updates} ${qsTr(")")}`; - notify(qsTr("Updates Available"), msg); - lastNotifiedUpdates = updates; + + updateInProgress = true; + Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"]); + + // Refresh after updates with multiple attempts + refreshAfterUpdate(); } - + + // Update selected packages + function runSelectiveUpdate() { + if (selectedPackages.length === 0) return; + + updateInProgress = true; + const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages); + Quickshell.execDetached(command); + + // Clear selection and refresh + selectedPackages = []; + selectedPackagesCount = 0; + refreshAfterUpdate(); + } + + // Package selection functions + function togglePackageSelection(packageName) { + const index = selectedPackages.indexOf(packageName); + if (index > -1) { + selectedPackages.splice(index, 1); + } else { + selectedPackages.push(packageName); + } + selectedPackagesCount = selectedPackages.length; + } + + function selectAllPackages() { + selectedPackages = updatePackages.map(pkg => pkg.name); + selectedPackagesCount = selectedPackages.length; + } + + function deselectAllPackages() { + selectedPackages = []; + selectedPackagesCount = 0; + } + + function isPackageSelected(packageName) { + return selectedPackages.indexOf(packageName) > -1; + } + + // Robust refresh after updates + function refreshAfterUpdate() { + // First refresh attempt after 3 seconds + Qt.callLater(() => { + doPoll(); + }, 3000); + + // Second refresh attempt after 8 seconds + Qt.callLater(() => { + doPoll(); + }, 8000); + + // Third refresh attempt after 15 seconds + Qt.callLater(() => { + doPoll(); + updateInProgress = false; + }, 15000); + + // Final refresh attempt after 30 seconds + Qt.callLater(() => { + doPoll(); + }, 30000); + } + + // Notification helper + function notify(title, body) { + Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]); + } + + // Auto-poll every 15 minutes Timer { - id: pollTimer - interval: updateService.pollInterval + interval: 15 * 60 * 1000 // 15 minutes repeat: true - onTriggered: { - if (!updateService.ready) - return; - updateService.doPoll(); - } - } - - Timer { - id: killTimer - interval: updateService.lastWasFull ? updateService.minuteMs : updateService.quickTimeoutMs - repeat: false - onTriggered: { - if (pkgProc.running) { - console.error("[UpdateService] Update check killed (timeout)"); - updateService.notify(qsTr("Update check killed"), qsTr("Process took too long")); - } - } + running: true + onTriggered: doPoll() } + + // Initial check + Component.onCompleted: doPoll() } diff --git a/Services/PanelService.qml b/Services/PanelService.qml index e2d82f7..435a45a 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -11,6 +11,9 @@ Singleton { // A ref. to the lockScreen, so it's accessible from other services property var lockScreen: null + // A ref. to the updatePanel, so it's accessible from other services + property var updatePanel: null + // Currently opened panel property var openedPanel: null diff --git a/shell.qml b/shell.qml index 8ce8361..6eafab7 100644 --- a/shell.qml +++ b/shell.qml @@ -27,6 +27,7 @@ import qs.Modules.PowerPanel import qs.Modules.SidePanel import qs.Modules.Toast import qs.Modules.WiFiPanel +import qs.Modules.ArchUpdaterPanel import qs.Services import qs.Widgets @@ -79,6 +80,10 @@ ShellRoot { id: bluetoothPanel } + ArchUpdaterPanel { + id: updatePanel + } + ToastManager {} IPCManager {} @@ -90,6 +95,9 @@ ShellRoot { // Save a ref. to our lockScreen so we can access it from services PanelService.lockScreen = lockScreen + // Save a ref. to our updatePanel so we can access it from services + PanelService.updatePanel = updatePanel + // Ensure our singleton is created as soon as possible so we start fetching weather asap LocationService.init() } From 9ba5abc047f8a51a914bf7503cfc372d368143c7 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 16:36:17 +0200 Subject: [PATCH 39/54] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 019fcf0..60a09e9 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Features a modern modular architecture with a status bar, notification system, c - `gpu-screen-recorder` - Screen recording functionality - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) +If you want to use the ArchUpdater Widget, make sure you have any polkit agent installed. --- From 9666ce4f5a520b76297427656a39397e7a5ceb11 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 10:46:28 -0400 Subject: [PATCH 40/54] autofmt arch stuff --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 403 +++++++++--------- Modules/Bar/Widgets/ArchUpdater.qml | 104 ++--- Services/ArchUpdaterService.qml | 285 +++++++------ 3 files changed, 399 insertions(+), 393 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 70a23a3..dbf0f59 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -8,220 +8,221 @@ import qs.Services import qs.Widgets NPanel { - id: root - panelWidth: 380 * scaling - panelHeight: 500 * scaling - panelAnchorRight: true + id: root + panelWidth: 380 * scaling + panelHeight: 500 * scaling + panelAnchorRight: true - // Auto-refresh when service updates - Connections { - target: ArchUpdaterService - function onUpdatePackagesChanged() { - // Force UI update when packages change - if (root.visible) { - // Small delay to ensure data is fully updated - Qt.callLater(() => { - // Force a UI update by triggering a property change - ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages; - }, 100); - } - } + // Auto-refresh when service updates + Connections { + target: ArchUpdaterService + function onUpdatePackagesChanged() { + // Force UI update when packages change + if (root.visible) { + // Small delay to ensure data is fully updated + Qt.callLater(() => { + // Force a UI update by triggering a property change + ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages + }, 100) + } } + } - panelContent: Rectangle { - color: Color.mSurface - radius: Style.radiusL * scaling + panelContent: Rectangle { + color: Color.mSurface + radius: Style.radiusL * scaling - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginM * scaling + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NIcon { + text: "system_update" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary + } + + Text { + text: "System Updates" + font.pointSize: Style.fontSizeL * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: "Close" + sizeMultiplier: 0.8 + onClicked: root.close() + } + } + + NDivider { + Layout.fillWidth: true + } + + // Update summary + Text { + text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length + !== 1 ? "s" : "") + " can be updated" + font.pointSize: Style.fontSizeL * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true + } + + // Package selection info + Text { + text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected" + font.pointSize: Style.fontSizeS * scaling + font.family: Settings.data.ui.fontDefault + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + } + + // Package list + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurfaceVariant + radius: Style.radiusM * scaling + + ListView { + id: packageListView + anchors.fill: parent + anchors.margins: Style.marginS * scaling + clip: true + model: ArchUpdaterService.updatePackages + spacing: Style.marginXS * scaling + + delegate: Rectangle { + width: packageListView.width + height: 50 * scaling + color: Color.transparent + radius: Style.radiusS * scaling - // Header RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling - NIcon { - text: "system_update" - font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary + // Checkbox for selection + NIconButton { + id: checkbox + icon: "check_box_outline_blank" + onClicked: { + const isSelected = ArchUpdaterService.isPackageSelected(modelData.name) + if (isSelected) { + ArchUpdaterService.togglePackageSelection(modelData.name) + icon = "check_box_outline_blank" + colorFg = Color.mOnSurfaceVariant + } else { + ArchUpdaterService.togglePackageSelection(modelData.name) + icon = "check_box" + colorFg = Color.mPrimary + } + } + colorBg: Color.transparent + colorFg: Color.mOnSurfaceVariant + Layout.preferredWidth: 30 * scaling + Layout.preferredHeight: 30 * scaling + + Component.onCompleted: { + // Set initial state + if (ArchUpdaterService.isPackageSelected(modelData.name)) { + icon = "check_box" + colorFg = Color.mPrimary + } + } + } + + // Package info + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS * scaling + + Text { + text: modelData.name + font.pointSize: Style.fontSizeM * scaling + font.family: Settings.data.ui.fontDefault + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true } Text { - text: "System Updates" - font.pointSize: Style.fontSizeL * scaling - font.family: Settings.data.ui.fontDefault - font.weight: Style.fontWeightBold - color: Color.mOnSurface - Layout.fillWidth: true - } - - NIconButton { - icon: "close" - tooltipText: "Close" - sizeMultiplier: 0.8 - onClicked: root.close() + text: modelData.oldVersion + " → " + modelData.newVersion + font.pointSize: Style.fontSizeS * scaling + font.family: Settings.data.ui.fontDefault + color: Color.mOnSurfaceVariant + Layout.fillWidth: true } + } } + } - NDivider { Layout.fillWidth: true } - - // Update summary - Text { - text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length !== 1 ? "s" : "") + " can be updated" - font.pointSize: Style.fontSizeL * scaling - font.family: Settings.data.ui.fontDefault - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - Layout.fillWidth: true - } - - // Package selection info - Text { - text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected" - font.pointSize: Style.fontSizeS * scaling - font.family: Settings.data.ui.fontDefault - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - } - - // Package list - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Color.mSurfaceVariant - radius: Style.radiusM * scaling - - ListView { - id: packageListView - anchors.fill: parent - anchors.margins: Style.marginS * scaling - clip: true - model: ArchUpdaterService.updatePackages - spacing: Style.marginXS * scaling - - delegate: Rectangle { - width: packageListView.width - height: 50 * scaling - color: Color.transparent - radius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling - - // Checkbox for selection - NIconButton { - id: checkbox - icon: "check_box_outline_blank" - onClicked: { - const isSelected = ArchUpdaterService.isPackageSelected(modelData.name); - if (isSelected) { - ArchUpdaterService.togglePackageSelection(modelData.name); - icon = "check_box_outline_blank"; - colorFg = Color.mOnSurfaceVariant; - } else { - ArchUpdaterService.togglePackageSelection(modelData.name); - icon = "check_box"; - colorFg = Color.mPrimary; - } - } - colorBg: Color.transparent - colorFg: Color.mOnSurfaceVariant - Layout.preferredWidth: 30 * scaling - Layout.preferredHeight: 30 * scaling - - Component.onCompleted: { - // Set initial state - if (ArchUpdaterService.isPackageSelected(modelData.name)) { - icon = "check_box"; - colorFg = Color.mPrimary; - } - } - } - - // Package info - ColumnLayout { - Layout.fillWidth: true - spacing: Style.marginXXS * scaling - - Text { - text: modelData.name - font.pointSize: Style.fontSizeM * scaling - font.family: Settings.data.ui.fontDefault - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - Layout.fillWidth: true - } - - Text { - text: modelData.oldVersion + " → " + modelData.newVersion - font.pointSize: Style.fontSizeS * scaling - font.family: Settings.data.ui.fontDefault - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - } - } - } - } - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - } - } - - - - // Action buttons - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS * scaling - - NIconButton { - icon: "refresh" - tooltipText: "Check for updates" - onClicked: { - ArchUpdaterService.doPoll(); - } - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - Layout.fillWidth: true - Layout.preferredHeight: 35 * scaling - } - - NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update" - tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" - enabled: !ArchUpdaterService.updateInProgress - onClicked: { - ArchUpdaterService.runUpdate(); - } - colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary - colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary - Layout.fillWidth: true - Layout.preferredHeight: 35 * scaling - } - - NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings" - tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" - enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 - onClicked: { - if (ArchUpdaterService.selectedPackagesCount > 0) { - ArchUpdaterService.runSelectiveUpdate(); - } - } - colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : - (ArchUpdaterService.selectedPackagesCount > 0 ? Color.mSecondary : Color.mSurfaceVariant) - colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : - (ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant) - Layout.fillWidth: true - Layout.preferredHeight: 35 * scaling - } - } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } } + } + + // Action buttons + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NIconButton { + icon: "refresh" + tooltipText: "Check for updates" + onClicked: { + ArchUpdaterService.doPoll() + } + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + + NIconButton { + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" + enabled: !ArchUpdaterService.updateInProgress + onClicked: { + ArchUpdaterService.runUpdate() + } + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + + NIconButton { + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings" + tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" + enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 + onClicked: { + if (ArchUpdaterService.selectedPackagesCount > 0) { + ArchUpdaterService.runSelectiveUpdate() + } + } + colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount + > 0 ? Color.mSecondary : Color.mSurfaceVariant) + colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount + > 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant) + Layout.fillWidth: true + Layout.preferredHeight: 35 * scaling + } + } } + } } diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 4881966..6b09817 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -6,66 +6,68 @@ import QtQuick.Controls import QtQuick.Layouts NIconButton { - id: root - sizeMultiplier: 0.8 + id: root + sizeMultiplier: 0.8 - readonly property real scaling: ScalingService.scale(screen) + readonly property real scaling: ScalingService.scale(screen) - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.transparent + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.transparent - // Enhanced icon states with better visual feedback - icon: { - if (ArchUpdaterService.busy) return "sync" - if (ArchUpdaterService.updatePackages.length > 0) { - // Show different icons based on update count - const count = ArchUpdaterService.updatePackages.length - if (count > 50) return "system_update_alt" // Many updates - if (count > 10) return "system_update" // Moderate updates - return "system_update" // Few updates - } - return "task_alt" + // Enhanced icon states with better visual feedback + icon: { + if (ArchUpdaterService.busy) + return "sync" + if (ArchUpdaterService.updatePackages.length > 0) { + // Show different icons based on update count + const count = ArchUpdaterService.updatePackages.length + if (count > 50) + return "system_update_alt" // Many updates + if (count > 10) + return "system_update" // Moderate updates + return "system_update" // Few updates } + return "task_alt" + } - // Enhanced tooltip with more information - tooltipText: { - if (ArchUpdaterService.busy) - return "Checking for updates…"; + // Enhanced tooltip with more information + tooltipText: { + if (ArchUpdaterService.busy) + return "Checking for updates…" - var count = ArchUpdaterService.updatePackages.length; - if (count === 0) - return "System is up to date ✓"; + var count = ArchUpdaterService.updatePackages.length + if (count === 0) + return "System is up to date ✓" - var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:"); + var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:") - var list = ArchUpdaterService.updatePackages || []; - var s = ""; - var limit = Math.min(list.length, 8); // Reduced to 8 for better readability - for (var i = 0; i < limit; ++i) { - var p = list[i]; - s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion); - } - if (list.length > 8) - s += "\n… and " + (list.length - 8) + " more"; - - return header + "\n\n" + s + "\n\nClick to update system"; + var list = ArchUpdaterService.updatePackages || [] + var s = "" + var limit = Math.min(list.length, 8) + // Reduced to 8 for better readability + for (var i = 0; i < limit; ++i) { + var p = list[i] + s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion) } + if (list.length > 8) + s += "\n… and " + (list.length - 8) + " more" - // Enhanced click behavior with confirmation - onClicked: { - if (ArchUpdaterService.busy) - return; - - if (ArchUpdaterService.updatePackages.length > 0) { - // Show confirmation dialog for updates - PanelService.updatePanel.toggle(screen); - } else { - // Just refresh if no updates available - ArchUpdaterService.doPoll(); - } + return header + "\n\n" + s + "\n\nClick to update system" + } + + // Enhanced click behavior with confirmation + onClicked: { + if (ArchUpdaterService.busy) + return + + if (ArchUpdaterService.updatePackages.length > 0) { + // Show confirmation dialog for updates + PanelService.updatePanel.toggle(screen) + } else { + // Just refresh if no updates available + ArchUpdaterService.doPoll() } - - + } } diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index ed392ef..611ce59 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -1,153 +1,156 @@ pragma Singleton + import Quickshell import QtQuick import Quickshell.Io Singleton { - id: updateService - - // Core properties - readonly property bool busy: checkupdatesProcess.running - readonly property int updates: updatePackages.length - property var updatePackages: [] - property var selectedPackages: [] - property int selectedPackagesCount: 0 - property bool updateInProgress: false - - // Process for checking updates - Process { - id: checkupdatesProcess - command: ["checkupdates"] - onExited: function(exitCode) { - if (exitCode !== 0 && exitCode !== 2) { - console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")"); - updatePackages = []; - return; - } - } - stdout: StdioCollector { - onStreamFinished: { - parseCheckupdatesOutput(text); - } - } + id: updateService + + // Core properties + readonly property bool busy: checkupdatesProcess.running + readonly property int updates: updatePackages.length + property var updatePackages: [] + property var selectedPackages: [] + property int selectedPackagesCount: 0 + property bool updateInProgress: false + + // Process for checking updates + Process { + id: checkupdatesProcess + command: ["checkupdates"] + onExited: function (exitCode) { + if (exitCode !== 0 && exitCode !== 2) { + console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")") + updatePackages = [] + return + } } - - // Parse checkupdates output - function parseCheckupdatesOutput(output) { - const lines = output.trim().split('\n').filter(line => line.trim()); - const packages = []; - - for (const line of lines) { - const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/); - if (m) { - packages.push({ - name: m[1], - oldVersion: m[2], - newVersion: m[3], - description: `${m[1]} ${m[2]} -> ${m[3]}` - }); - } - } - - updatePackages = packages; + stdout: StdioCollector { + onStreamFinished: { + parseCheckupdatesOutput(text) + } } - - // Check for updates - function doPoll() { - if (busy) return; - checkupdatesProcess.running = true; + } + + // Parse checkupdates output + function parseCheckupdatesOutput(output) { + const lines = output.trim().split('\n').filter(line => line.trim()) + const packages = [] + + for (const line of lines) { + const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/) + if (m) { + packages.push({ + "name": m[1], + "oldVersion": m[2], + "newVersion": m[3], + "description": `${m[1]} ${m[2]} -> ${m[3]}` + }) + } } - - // Update all packages - function runUpdate() { - if (updates === 0) { - doPoll(); - return; - } - - updateInProgress = true; - Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"]); - - // Refresh after updates with multiple attempts - refreshAfterUpdate(); + + updatePackages = packages + } + + // Check for updates + function doPoll() { + if (busy) + return + checkupdatesProcess.running = true + } + + // Update all packages + function runUpdate() { + if (updates === 0) { + doPoll() + return } - - // Update selected packages - function runSelectiveUpdate() { - if (selectedPackages.length === 0) return; - - updateInProgress = true; - const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages); - Quickshell.execDetached(command); - - // Clear selection and refresh - selectedPackages = []; - selectedPackagesCount = 0; - refreshAfterUpdate(); + + updateInProgress = true + Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"]) + + // Refresh after updates with multiple attempts + refreshAfterUpdate() + } + + // Update selected packages + function runSelectiveUpdate() { + if (selectedPackages.length === 0) + return + + updateInProgress = true + const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages) + Quickshell.execDetached(command) + + // Clear selection and refresh + selectedPackages = [] + selectedPackagesCount = 0 + refreshAfterUpdate() + } + + // Package selection functions + function togglePackageSelection(packageName) { + const index = selectedPackages.indexOf(packageName) + if (index > -1) { + selectedPackages.splice(index, 1) + } else { + selectedPackages.push(packageName) } - - // Package selection functions - function togglePackageSelection(packageName) { - const index = selectedPackages.indexOf(packageName); - if (index > -1) { - selectedPackages.splice(index, 1); - } else { - selectedPackages.push(packageName); - } - selectedPackagesCount = selectedPackages.length; - } - - function selectAllPackages() { - selectedPackages = updatePackages.map(pkg => pkg.name); - selectedPackagesCount = selectedPackages.length; - } - - function deselectAllPackages() { - selectedPackages = []; - selectedPackagesCount = 0; - } - - function isPackageSelected(packageName) { - return selectedPackages.indexOf(packageName) > -1; - } - - // Robust refresh after updates - function refreshAfterUpdate() { - // First refresh attempt after 3 seconds - Qt.callLater(() => { - doPoll(); - }, 3000); - - // Second refresh attempt after 8 seconds - Qt.callLater(() => { - doPoll(); - }, 8000); - - // Third refresh attempt after 15 seconds - Qt.callLater(() => { - doPoll(); - updateInProgress = false; - }, 15000); - - // Final refresh attempt after 30 seconds - Qt.callLater(() => { - doPoll(); - }, 30000); - } - - // Notification helper - function notify(title, body) { - Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]); - } - - // Auto-poll every 15 minutes - Timer { - interval: 15 * 60 * 1000 // 15 minutes - repeat: true - running: true - onTriggered: doPoll() - } - - // Initial check - Component.onCompleted: doPoll() + selectedPackagesCount = selectedPackages.length + } + + function selectAllPackages() { + selectedPackages = updatePackages.map(pkg => pkg.name) + selectedPackagesCount = selectedPackages.length + } + + function deselectAllPackages() { + selectedPackages = [] + selectedPackagesCount = 0 + } + + function isPackageSelected(packageName) { + return selectedPackages.indexOf(packageName) > -1 + } + + // Robust refresh after updates + function refreshAfterUpdate() { + // First refresh attempt after 3 seconds + Qt.callLater(() => { + doPoll() + }, 3000) + + // Second refresh attempt after 8 seconds + Qt.callLater(() => { + doPoll() + }, 8000) + + // Third refresh attempt after 15 seconds + Qt.callLater(() => { + doPoll() + updateInProgress = false + }, 15000) + + // Final refresh attempt after 30 seconds + Qt.callLater(() => { + doPoll() + }, 30000) + } + + // Notification helper + function notify(title, body) { + Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]) + } + + // Auto-poll every 15 minutes + Timer { + interval: 15 * 60 * 1000 // 15 minutes + repeat: true + running: true + onTriggered: doPoll() + } + + // Initial check + Component.onCompleted: doPoll() } From 3db3d1deb00f490c69d1ea895b7f92e33cbf5369 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 16:56:09 +0200 Subject: [PATCH 41/54] Close ArchUpdaterPanel when clicking on the update buttons --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index dbf0f59..d89e511 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -199,6 +199,7 @@ NPanel { enabled: !ArchUpdaterService.updateInProgress onClicked: { ArchUpdaterService.runUpdate() + root.close() } colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary @@ -213,6 +214,7 @@ NPanel { onClicked: { if (ArchUpdaterService.selectedPackagesCount > 0) { ArchUpdaterService.runSelectiveUpdate() + root.close() } } colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount From f1a8624945ad03196872797e6abc3fdcffb3f47b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 17:37:50 +0200 Subject: [PATCH 42/54] Avoid using Qt.createQmlObject --- Modules/LockScreen/LockScreen.qml | 25 +++-- Services/ToastService.qml | 151 +++++++++++++++--------------- 2 files changed, 97 insertions(+), 79 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index db7686f..1376d31 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -48,6 +48,22 @@ Loader { property string password: "" property bool pamAvailable: typeof PamContext !== "undefined" + // Process components for system commands + Process { + id: shutdownProcess + command: ["shutdown", "-h", "now"] + } + + Process { + id: rebootProcess + command: ["reboot"] + } + + Process { + id: logoutProcess + command: ["loginctl", "terminate-user", Quickshell.env("USER")] + } + function unlockAttempt() { Logger.log("LockScreen", "Unlock attempt started") @@ -916,8 +932,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - Qt.createQmlObject( - 'import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock) + shutdownProcess.running = true } } @@ -971,7 +986,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock) + rebootProcess.running = true } } @@ -1025,9 +1040,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - Qt.createQmlObject( - 'import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env( - "USER") + '"]; running: true }', lock) + logoutProcess.running = true } } diff --git a/Services/ToastService.qml b/Services/ToastService.qml index b08a366..8ca18a2 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -1,10 +1,11 @@ pragma Singleton import QtQuick +import Quickshell import Quickshell.Io import qs.Commons -QtObject { +Singleton { id: root // Queue of pending toast messages @@ -14,6 +15,64 @@ QtObject { // Reference to the current toast instance (set by ToastManager) property var currentToast: null + // Properties for command checking + property var commandCheckCallback: null + property string commandCheckSuccessMessage: "" + property string commandCheckFailMessage: "" + + // Properties for command running + property var commandRunCallback: null + property string commandRunSuccessMessage: "" + property string commandRunFailMessage: "" + + // Properties for delayed toast + property string delayedToastMessage: "" + property string delayedToastType: "notice" + + // Process for command checking + Process { + id: commandCheckProcess + command: ["which", "test"] + onExited: function (exitCode) { + if (exitCode === 0) { + showNotice(commandCheckSuccessMessage) + if (commandCheckCallback) + commandCheckCallback() + } else { + showWarning(commandCheckFailMessage) + } + } + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Process for command running + Process { + id: commandRunProcess + command: ["echo", "test"] + onExited: function (exitCode) { + if (exitCode === 0) { + showNotice(commandRunSuccessMessage) + if (commandRunCallback) + commandRunCallback() + } else { + showWarning(commandRunFailMessage) + } + } + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Timer for delayed toast + Timer { + id: delayedToastTimer + interval: 1000 + repeat: false + onTriggered: { + showToast(delayedToastMessage, delayedToastType) + } + } + // Methods to show different types of messages function showNotice(label, description = "", persistent = false, duration = 3000) { showToast(label, description, "notice", persistent, duration) @@ -25,37 +84,14 @@ QtObject { // Utility function to check if a command exists and show appropriate toast function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) { - var checkProcess = Qt.createQmlObject(` - import QtQuick - import Quickshell.Io - Process { - id: checkProc - command: ["which", "${command}"] - running: true + // Store callback for use in the process + commandCheckCallback = onSuccess + commandCheckSuccessMessage = successMessage + commandCheckFailMessage = failMessage - property var onSuccessCallback: null - property bool hasFinished: false - - onExited: { - if (!hasFinished) { - hasFinished = true - if (exitCode === 0) { - ToastService.showNotice("${successMessage}") - if (onSuccessCallback) onSuccessCallback() - } else { - ToastService.showWarning("${failMessage}") - } - checkProc.destroy() - } - } - - // Fallback collectors to prevent issues - stdout: StdioCollector {} - stderr: StdioCollector {} - } - `, root) - - checkProcess.onSuccessCallback = onSuccess + // Start the command check process + commandCheckProcess.command = ["which", command] + commandCheckProcess.running = true } // Simple function to show a random toast (useful for testing or fun messages) @@ -95,37 +131,14 @@ QtObject { // Generic command runner with toast feedback function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) { - var fullCommand = [command].concat(args || []) - var runProcess = Qt.createQmlObject(` - import QtQuick - import Quickshell.Io - Process { - id: runProc - command: ${JSON.stringify(fullCommand)} - running: true + // Store callback for use in the process + commandRunCallback = onSuccess + commandRunSuccessMessage = successMessage + commandRunFailMessage = failMessage - property var onSuccessCallback: null - property bool hasFinished: false - - onExited: { - if (!hasFinished) { - hasFinished = true - if (exitCode === 0) { - ToastService.showNotice("${successMessage}") - if (onSuccessCallback) onSuccessCallback() - } else { - ToastService.showWarning("${failMessage}") - } - runProc.destroy() - } - } - - stdout: StdioCollector {} - stderr: StdioCollector {} - } - `, root) - - runProcess.onSuccessCallback = onSuccess + // Start the command run process + commandRunProcess.command = [command].concat(args || []) + commandRunProcess.running = true } // Check if a file/directory exists @@ -135,18 +148,10 @@ QtObject { // Show toast after a delay (useful for delayed feedback) function delayedToast(message, type = "notice", delayMs = 1000) { - var timer = Qt.createQmlObject(` - import QtQuick - Timer { - interval: ${delayMs} - repeat: false - running: true - onTriggered: { - ToastService.showToast("${message}", "${type}") - destroy() - } - } - `, root) + delayedToastMessage = message + delayedToastType = type + delayedToastTimer.interval = delayMs + delayedToastTimer.restart() } // Generic method to show a toast From e3154fb9a50e20beacdaabff9c35e2f6eea29de3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 17:47:42 +0200 Subject: [PATCH 43/54] Possible Toast fix --- Modules/Toast/ToastManager.qml | 2 +- Services/ToastService.qml | 51 ++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml index 35fe01e..ea65b00 100644 --- a/Modules/Toast/ToastManager.qml +++ b/Modules/Toast/ToastManager.qml @@ -52,7 +52,7 @@ Variants { Component.onCompleted: { // Register this toast with the service - ToastService.currentToast = toast + ToastService.allToasts.push(toast) // Connect dismissal signal toast.dismissed.connect(ToastService.onToastDismissed) diff --git a/Services/ToastService.qml b/Services/ToastService.qml index 8ca18a2..38ce2db 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -12,8 +12,8 @@ Singleton { property var messageQueue: [] property bool isShowingToast: false - // Reference to the current toast instance (set by ToastManager) - property var currentToast: null + // Reference to all toast instances (set by ToastManager) + property var allToasts: [] // Properties for command checking property var commandCheckCallback: null @@ -176,7 +176,7 @@ Singleton { // Process the message queue function processQueue() { - if (messageQueue.length === 0 || !currentToast) { + if (messageQueue.length === 0 || allToasts.length === 0) { isShowingToast = false return } @@ -189,24 +189,37 @@ Singleton { var toastData = messageQueue.shift() isShowingToast = true - // Configure and show toast - currentToast.label = toastData.label - currentToast.description = toastData.description - currentToast.type = toastData.type - currentToast.persistent = toastData.persistent - currentToast.duration = toastData.duration - currentToast.show() + // Configure and show toast on all screens + for (var i = 0; i < allToasts.length; i++) { + var toast = allToasts[i] + toast.label = toastData.label + toast.description = toastData.description + toast.type = toastData.type + toast.persistent = toastData.persistent + toast.duration = toastData.duration + toast.show() + } } // Called when a toast is dismissed function onToastDismissed() { + // Check if all toasts are dismissed + var allDismissed = true + for (var i = 0; i < allToasts.length; i++) { + if (allToasts[i].visible) { + allDismissed = false + break + } + } + + if (allDismissed) { + isShowingToast = false - isShowingToast = false - - // Small delay before showing next toast - Qt.callLater(function () { - processQueue() - }) + // Small delay before showing next toast + Qt.callLater(function () { + processQueue() + }) + } } // Clear all pending messages @@ -217,8 +230,10 @@ Singleton { // Hide current toast function hideCurrentToast() { - if (currentToast && isShowingToast) { - currentToast.hide() + if (isShowingToast) { + for (var i = 0; i < allToasts.length; i++) { + allToasts[i].hide() + } } } From b8b660db08fe80c08d7fdaaaf49f150b1d2ab474 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 17:55:17 +0200 Subject: [PATCH 44/54] Another possible toast fix --- Modules/Toast/ToastManager.qml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml index ea65b00..8bb8706 100644 --- a/Modules/Toast/ToastManager.qml +++ b/Modules/Toast/ToastManager.qml @@ -17,6 +17,10 @@ Variants { readonly property real scaling: ScalingService.scale(screen) screen: modelData + // Only show on screens that have notifications enabled + visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) + || (Settings.data.notifications.monitors.length === 0)) : false + // Position based on bar location, like Notification popup does anchors { top: Settings.data.bar.position === "top" @@ -51,11 +55,15 @@ Variants { hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20 Component.onCompleted: { - // Register this toast with the service - ToastService.allToasts.push(toast) + // Only register toasts for screens that have notifications enabled + if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name) + || (Settings.data.notifications.monitors.length === 0)) : false) { + // Register this toast with the service + ToastService.allToasts.push(toast) - // Connect dismissal signal - toast.dismissed.connect(ToastService.onToastDismissed) + // Connect dismissal signal + toast.dismissed.connect(ToastService.onToastDismissed) + } } } } From 89d0551f3e74956c4c8b06916d32a0392592f76b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 18:25:17 +0200 Subject: [PATCH 45/54] Use CompositorService power commands in LockScreen --- Modules/LockScreen/LockScreen.qml | 22 +++------------------- Modules/Toast/ToastManager.qml | 4 ++-- Services/ToastService.qml | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 1376d31..c415d03 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -48,22 +48,6 @@ Loader { property string password: "" property bool pamAvailable: typeof PamContext !== "undefined" - // Process components for system commands - Process { - id: shutdownProcess - command: ["shutdown", "-h", "now"] - } - - Process { - id: rebootProcess - command: ["reboot"] - } - - Process { - id: logoutProcess - command: ["loginctl", "terminate-user", Quickshell.env("USER")] - } - function unlockAttempt() { Logger.log("LockScreen", "Unlock attempt started") @@ -932,7 +916,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - shutdownProcess.running = true + CompositorService.shutdown() } } @@ -986,7 +970,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - rebootProcess.running = true + CompositorService.reboot() } } @@ -1040,7 +1024,7 @@ Loader { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - logoutProcess.running = true + CompositorService.logout() } } diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml index 8bb8706..0da45c1 100644 --- a/Modules/Toast/ToastManager.qml +++ b/Modules/Toast/ToastManager.qml @@ -19,7 +19,7 @@ Variants { // Only show on screens that have notifications enabled visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false + || (Settings.data.notifications.monitors.length === 0)) : false // Position based on bar location, like Notification popup does anchors { @@ -57,7 +57,7 @@ Variants { Component.onCompleted: { // Only register toasts for screens that have notifications enabled if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false) { + || (Settings.data.notifications.monitors.length === 0)) : false) { // Register this toast with the service ToastService.allToasts.push(toast) diff --git a/Services/ToastService.qml b/Services/ToastService.qml index 38ce2db..5db7139 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -211,7 +211,7 @@ Singleton { break } } - + if (allDismissed) { isShowingToast = false From 4d8cf2207d42cd6af006240bda42b12b2a71f297 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 18:57:15 +0200 Subject: [PATCH 46/54] Small change in LockScreen --- Modules/LockScreen/LockScreen.qml | 6 ++++-- Modules/Toast/ToastManager.qml | 4 ++-- Services/ToastService.qml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index c415d03..674c471 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -48,6 +48,8 @@ Loader { property string password: "" property bool pamAvailable: typeof PamContext !== "undefined" + + function unlockAttempt() { Logger.log("LockScreen", "Unlock attempt started") @@ -626,7 +628,7 @@ Loader { spacing: Style.marginM * scaling NText { - text: "root@noctalia:~$" + text: Quickshell.env("USER") + "@noctalia:~$" color: Color.mPrimary font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling @@ -664,7 +666,7 @@ Loader { spacing: Style.marginM * scaling NText { - text: "root@noctalia:~$" + text: Quickshell.env("USER") + "@noctalia:~$" color: Color.mPrimary font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeL * scaling diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml index 0da45c1..8bb8706 100644 --- a/Modules/Toast/ToastManager.qml +++ b/Modules/Toast/ToastManager.qml @@ -19,7 +19,7 @@ Variants { // Only show on screens that have notifications enabled visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false + || (Settings.data.notifications.monitors.length === 0)) : false // Position based on bar location, like Notification popup does anchors { @@ -57,7 +57,7 @@ Variants { Component.onCompleted: { // Only register toasts for screens that have notifications enabled if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false) { + || (Settings.data.notifications.monitors.length === 0)) : false) { // Register this toast with the service ToastService.allToasts.push(toast) diff --git a/Services/ToastService.qml b/Services/ToastService.qml index 5db7139..38ce2db 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -211,7 +211,7 @@ Singleton { break } } - + if (allDismissed) { isShowingToast = false From 76626dc8da5f31d8ec32032a048cb9a2bc6cba74 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 19:38:20 +0200 Subject: [PATCH 47/54] Rework LockScreen auth logic --- Modules/LockScreen/LockContext.qml | 92 ++ Modules/LockScreen/LockScreen.qml | 1685 +++++++++++----------------- 2 files changed, 770 insertions(+), 1007 deletions(-) create mode 100644 Modules/LockScreen/LockContext.qml diff --git a/Modules/LockScreen/LockContext.qml b/Modules/LockScreen/LockContext.qml new file mode 100644 index 0000000..541fbe7 --- /dev/null +++ b/Modules/LockScreen/LockContext.qml @@ -0,0 +1,92 @@ +import QtQuick +import Quickshell +import Quickshell.Services.Pam + +Scope { + id: root + signal unlocked() + signal failed() + + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + property string errorMessage: "" + property bool pamAvailable: typeof PamContext !== "undefined" + + onCurrentTextChanged: { + if (currentText !== "") { + showFailure = false; + errorMessage = ""; + } + } + + function tryUnlock() { + if (!pamAvailable) { + errorMessage = "PAM not available"; + showFailure = true; + return; + } + + if (currentText === "") { + errorMessage = "Password required"; + showFailure = true; + return; + } + + root.unlockInProgress = true; + errorMessage = ""; + showFailure = false; + + console.log("Starting PAM authentication for user:", pam.user); + pam.start(); + } + + PamContext { + id: pam + config: "login" + user: Quickshell.env("USER") + + onPamMessage: { + console.log("PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired); + + if (messageIsError) { + errorMessage = message; + } + + if (responseRequired) { + console.log("Responding to PAM with password"); + respond(root.currentText); + } + } + + onResponseRequiredChanged: { + console.log("Response required changed:", responseRequired); + if (responseRequired && root.unlockInProgress) { + console.log("Automatically responding to PAM"); + respond(root.currentText); + } + } + + onCompleted: { + console.log("PAM completed with result:", result); + if (result === PamResult.Success) { + console.log("Authentication successful"); + root.unlocked(); + } else { + console.log("Authentication failed"); + errorMessage = "Authentication failed"; + showFailure = true; + root.failed(); + } + root.unlockInProgress = false; + } + + onError: { + console.log("PAM error:", error, "message:", message); + errorMessage = message || "Authentication error"; + showFailure = true; + root.unlockInProgress = false; + root.failed(); + } + } +} \ No newline at end of file diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 674c471..de2ea71 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -14,1050 +14,721 @@ import qs.Widgets import qs.Modules.Audio Loader { - id: lockScreen - active: false + id: lockScreen + active: false - // Log state changes to help debug lock screen issues - onActiveChanged: { - Logger.log("LockScreen", "State changed:", active) - } - - // Allow a small grace period after unlocking so the compositor releases the lock surfaces - Timer { - id: unloadAfterUnlockTimer - interval: 250 - repeat: false - onTriggered: { - Logger.log("LockScreen", "Unload timer triggered - deactivating") - lockScreen.active = false + Timer { + id: unloadAfterUnlockTimer + interval: 250 + repeat: false + onTriggered: { + lockScreen.active = false + } } - } - function scheduleUnloadAfterUnlock() { - Logger.log("LockScreen", "Scheduling unload after unlock") - unloadAfterUnlockTimer.start() - } - sourceComponent: Component { - WlSessionLock { - id: lock - // Tie session lock to loader visibility - locked: lockScreen.active - - property string errorMessage: "" - property bool authenticating: false - property string password: "" - property bool pamAvailable: typeof PamContext !== "undefined" - - - - function unlockAttempt() { - Logger.log("LockScreen", "Unlock attempt started") - - // Real PAM authentication - if (!pamAvailable) { - lock.errorMessage = "PAM authentication not available." - Logger.log("LockScreen", "PAM not available") - return - } - if (!lock.password) { - lock.errorMessage = "Password required." - Logger.log("LockScreen", "No password entered") - return - } - Logger.log("LockScreen", "Starting PAM authentication") - lock.authenticating = true - lock.errorMessage = "" - - Logger.log("LockScreen", "About to create PAM context with userName:", Quickshell.env("USER")) - var pam = Qt.createQmlObject( - 'import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', - lock) - Logger.log("LockScreen", "PamContext created", pam) - - pam.onCompleted.connect(function (result) { - Logger.log("LockScreen", "PAM completed with result:", result) - lock.authenticating = false - if (result === PamResult.Success) { - Logger.log("LockScreen", "Authentication successful, unlocking") - // First release the Wayland session lock, then unload after a short delay - lock.locked = false - lockScreen.scheduleUnloadAfterUnlock() - lock.password = "" - lock.errorMessage = "" - } else { - Logger.log("LockScreen", "Authentication failed") - lock.errorMessage = "Authentication failed." - lock.password = "" - } - pam.destroy() - }) - - pam.onError.connect(function (error) { - Logger.log("LockScreen", "PAM error:", error) - lock.authenticating = false - lock.errorMessage = pam.message || "Authentication error." - lock.password = "" - pam.destroy() - }) - - pam.onPamMessage.connect(function () { - Logger.log("LockScreen", "PAM message:", pam.message, "isError:", pam.messageIsError) - if (pam.messageIsError) { - lock.errorMessage = pam.message - } - }) - - pam.onResponseRequiredChanged.connect(function () { - Logger.log("LockScreen", "PAM response required:", pam.responseRequired) - if (pam.responseRequired && lock.authenticating) { - Logger.log("LockScreen", "Responding to PAM with password") - pam.respond(lock.password) - } - }) - - var started = pam.start() - Logger.log("LockScreen", "PAM start result:", started) - } - - WlSessionLockSurface { - // Battery indicator component - - // WlSessionLockSurface provides a screen variable for the current screen. - // Also we use a different scaling algorithm based on the resolution, as the design is full screen. - readonly property real scaling: ScalingService.dynamicScale(screen) + function scheduleUnloadAfterUnlock() { + unloadAfterUnlockTimer.start() + } + sourceComponent: Component { Item { - id: batteryIndicator + id: lockContainer - // Import UPower for battery data - property var battery: UPower.displayDevice - property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent - property real percent: isReady ? (battery.percentage * 100) : 0 - property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false - property bool batteryVisible: isReady && percent > 0 - - // Choose icon based on charge and charging state - function getIcon() { - if (!batteryVisible) - return "" - - if (charging) - return "battery_android_bolt" - - if (percent >= 95) - return "battery_android_full" - - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6" - if (percent >= 70) - return "battery_android_5" - if (percent >= 55) - return "battery_android_4" - if (percent >= 40) - return "battery_android_3" - if (percent >= 25) - return "battery_android_2" - if (percent >= 10) - return "battery_android_1" - if (percent >= 0) - return "battery_android_0" - } - } - - // Keyboard layout indicator component - Item { - id: keyboardLayout - - property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' - && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" - } - - // Wallpaper image - Image { - id: lockBgImage - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" - cache: true - smooth: true - mipmap: false - } - - // Blurred background - Rectangle { - anchors.fill: parent - color: Color.transparent - - // Simple blur effect - layer.enabled: true - layer.smooth: true - layer.samples: 4 - } - - // Animated gradient overlay - Rectangle { - anchors.fill: parent - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.rgba(0, 0, 0, 0.6) - } - GradientStop { - position: 0.3 - color: Qt.rgba(0, 0, 0, 0.3) - } - GradientStop { - position: 0.7 - color: Qt.rgba(0, 0, 0, 0.4) - } - GradientStop { - position: 1.0 - color: Qt.rgba(0, 0, 0, 0.7) - } - } - - // Subtle animated particles - Repeater { - model: 20 - Rectangle { - width: Math.random() * 4 + 2 - height: width - radius: width * 0.5 - color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) - x: Math.random() * parent.width - y: Math.random() * parent.height - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 0.8 - duration: 2000 + Math.random() * 3000 + // Create the lock context + LockContext { + id: lockContext + onUnlocked: { + lockSession.locked = false + lockScreen.scheduleUnloadAfterUnlock() + lockContext.currentText = "" } - NumberAnimation { - to: 0.1 - duration: 2000 + Math.random() * 3000 - } - } - } - } - } - - // Main content - Centered design - Item { - anchors.fill: parent - - // Top section - Time, date, and user info - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 80 * scaling - spacing: 40 * scaling - - // Time display - Large and prominent with pulse animation - Column { - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignHCenter - - NText { - id: timeText - text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: Settings.data.ui.fontBillboard - font.pointSize: Style.fontSizeXXXL * 6 * scaling - font.weight: Style.fontWeightBold - font.letterSpacing: -2 * scaling - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { - to: 1.02 - duration: 2000 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 2000 - easing.type: Easing.InOutQuad - } - } - } - - NText { - id: dateText - text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: Settings.data.ui.fontBillboard - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Font.Light - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - width: timeText.width - } } - // User section with animated avatar - Column { - spacing: Style.marginM * scaling - Layout.alignment: Qt.AlignHCenter + WlSessionLock { + id: lockSession + locked: lockScreen.active - // Animated avatar with glow effect or audio visualizer - Rectangle { - width: 108 * scaling - height: 108 * scaling - radius: width * 0.5 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderL * scaling) - anchors.horizontalCenter: parent.horizontalCenter - z: 10 + WlSessionLockSurface { + readonly property real scaling: ScalingService.dynamicScale(screen) - // Circular audio visualizer when music is playing - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling + Item { + id: batteryIndicator + property var battery: UPower.displayDevice + property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent + property real percent: isReady ? (battery.percentage * 100) : 0 + property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false + property bool batteryVisible: isReady && percent > 0 - sourceComponent: Item { - Repeater { - model: CavaService.values.length - - Rectangle { - property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI - property real linearRadius: 70 * scaling - property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling) - property real linearBarWidth: 3 * scaling - - width: linearBarWidth - height: linearBarLength - color: Color.mPrimary - radius: linearBarWidth * 0.5 - - x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5 - y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5 - - transform: Rotation { - origin.x: linearBarWidth * 0.5 - origin.y: linearBarLength * 0.5 - angle: (linearAngle * 180 / Math.PI) + 90 + function getIcon() { + if (!batteryVisible) return "" + if (charging) return "battery_android_bolt" + if (percent >= 95) return "battery_android_full" + if (percent >= 85) return "battery_android_6" + if (percent >= 70) return "battery_android_5" + if (percent >= 55) return "battery_android_4" + if (percent >= 40) return "battery_android_3" + if (percent >= 25) return "battery_android_2" + if (percent >= 10) return "battery_android_1" + if (percent >= 0) return "battery_android_0" } - } } - } - } - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling - - sourceComponent: Item { - Repeater { - model: CavaService.values.length * 2 - - Rectangle { - property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length - * 2 - 1 - index) - property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI - property real mirroredRadius: 70 * scaling - property real mirroredBarLength: Math.max(2, - CavaService.values[mirroredValueIndex] * 30 * scaling) - property real mirroredBarWidth: 3 * scaling - - width: mirroredBarWidth - height: mirroredBarLength - color: Color.mPrimary - radius: mirroredBarWidth * 0.5 - - x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5 - y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5 - - transform: Rotation { - origin.x: mirroredBarWidth * 0.5 - origin.y: mirroredBarLength * 0.5 - angle: (mirroredAngle * 180 / Math.PI) + 90 - } - } + Item { + id: keyboardLayout + property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" } - } - } - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } - sourceComponent: Item { - Canvas { - id: waveCanvas - anchors.fill: parent - antialiasing: true + Rectangle { + anchors.fill: parent + color: Color.transparent + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } - onPaint: { - var ctx = getContext("2d") - ctx.reset() - - if (CavaService.values.length === 0) { - return + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } + GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } + GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } } - ctx.strokeStyle = Color.mPrimary - ctx.lineWidth = 2 * scaling - ctx.lineCap = "round" + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height - var centerX = width * 0.5 - var centerY = height * 0.5 - var baseRadius = 60 * scaling - var maxAmplitude = 20 * scaling + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } + } + } + } + } - ctx.beginPath() + Item { + anchors.fill: parent - for (var i = 0; i <= CavaService.values.length; i++) { - var index = i % CavaService.values.length - var angle = (i / CavaService.values.length) * 2 * Math.PI - var amplitude = CavaService.values[index] * maxAmplitude - var radius = baseRadius + amplitude + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * scaling + spacing: 40 * scaling - var x = centerX + Math.cos(angle) * radius - var y = centerY + Math.sin(angle) * radius + Column { + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignHCenter - if (i === 0) { - ctx.moveTo(x, y) - } else { - ctx.lineTo(x, y) - } + NText { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: Settings.data.ui.fontBillboard + font.pointSize: Style.fontSizeXXXL * 6 * scaling + font.weight: Style.fontWeightBold + font.letterSpacing: -2 * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } + } + } + + NText { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: Settings.data.ui.fontBillboard + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Font.Light + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + Column { + spacing: Style.marginM * scaling + Layout.alignment: Qt.AlignHCenter + + Rectangle { + width: 108 * scaling + height: 108 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderL * scaling) + anchors.horizontalCenter: parent.horizontalCenter + z: 10 + + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Repeater { + model: CavaService.values.length + Rectangle { + property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI + property real linearRadius: 70 * scaling + property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling) + property real linearBarWidth: 3 * scaling + width: linearBarWidth + height: linearBarLength + color: Color.mPrimary + radius: linearBarWidth * 0.5 + x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5 + y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5 + transform: Rotation { + origin.x: linearBarWidth * 0.5 + origin.y: linearBarLength * 0.5 + angle: (linearAngle * 180 / Math.PI) + 90 + } + } + } + } + } + + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Repeater { + model: CavaService.values.length * 2 + Rectangle { + property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length * 2 - 1 - index) + property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI + property real mirroredRadius: 70 * scaling + property real mirroredBarLength: Math.max(2, CavaService.values[mirroredValueIndex] * 30 * scaling) + property real mirroredBarWidth: 3 * scaling + width: mirroredBarWidth + height: mirroredBarLength + color: Color.mPrimary + radius: mirroredBarWidth * 0.5 + x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5 + y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5 + transform: Rotation { + origin.x: mirroredBarWidth * 0.5 + origin.y: mirroredBarLength * 0.5 + angle: (mirroredAngle * 180 / Math.PI) + 90 + } + } + } + } + } + + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Canvas { + id: waveCanvas + anchors.fill: parent + antialiasing: true + onPaint: { + var ctx = getContext("2d") + ctx.reset() + if (CavaService.values.length === 0) return + ctx.strokeStyle = Color.mPrimary + ctx.lineWidth = 2 * scaling + ctx.lineCap = "round" + var centerX = width * 0.5 + var centerY = height * 0.5 + var baseRadius = 60 * scaling + var maxAmplitude = 20 * scaling + ctx.beginPath() + for (var i = 0; i <= CavaService.values.length; i++) { + var index = i % CavaService.values.length + var angle = (i / CavaService.values.length) * 2 * Math.PI + var amplitude = CavaService.values[index] * maxAmplitude + var radius = baseRadius + amplitude + var x = centerX + Math.cos(angle) * radius + var y = centerY + Math.sin(angle) * radius + if (i === 0) ctx.moveTo(x, y) + else ctx.lineTo(x, y) + } + ctx.closePath() + ctx.stroke() + } + } + Timer { + interval: 16 + running: true + repeat: true + onTriggered: waveCanvas.requestPaint() + } + } + } + + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * scaling + height: parent.height + 24 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + border.width: Math.max(1, Style.borderM * scaling) + z: -1 + visible: !MediaService.isPlaying + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } + } + } + + NImageCircled { + anchors.centerIn: parent + width: 100 * scaling + height: 100 * scaling + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.OutBack } + } + } + } } - ctx.closePath() - ctx.stroke() - } + Item { + width: 720 * scaling + height: 280 * scaling + anchors.centerIn: parent + anchors.verticalCenterOffset: 50 * scaling + + Item { + width: parent.width + height: 280 * scaling + Layout.fillWidth: true + + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: Style.radiusM * scaling + color: Color.applyOpacity(Color.mSurface, "E6") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Color.applyOpacity(Color.mPrimary, "1A") + y: index * 10 * scaling + opacity: Style.opacityMedium + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } + NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } + } + } + } + + Rectangle { + width: parent.width + height: 40 * scaling + color: Color.applyOpacity(Color.mPrimary, "33") + topLeftRadius: Style.radiusS * scaling + topRightRadius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.topMargin: Style.marginM * scaling + anchors.bottomMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginL * scaling + anchors.rightMargin: Style.marginL * scaling + spacing: Style.marginM * scaling + + NText { + text: "SECURE TERMINAL" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + Row { + spacing: Style.marginS * scaling + visible: batteryIndicator.batteryVisible + NIcon { + text: batteryIndicator.getIcon() + font.pointSize: Style.fontSizeM * scaling + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + } + + Row { + spacing: Style.marginS * scaling + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + NIcon { + text: "keyboard_alt" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + } + } + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Style.marginL * scaling + anchors.topMargin: 70 * scaling + spacing: Style.marginM * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + id: welcomeText + text: "" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + text: "sudo unlock-session" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + } + + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lockContext.currentText + onTextChanged: { + lockContext.currentText = text + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lockContext.tryUnlock() + } + } + + Component.onCompleted: { + forceActiveFocus() + } + } + + NText { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + visible: passwordInput.activeFocus + + SequentialAnimation { + id: typingEffect + NumberAnimation { target: passwordInput; property: "scale"; to: 1.01; duration: 50 } + NumberAnimation { target: passwordInput; property: "scale"; to: 1.0; duration: 50 } + } + } + + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + Layout.leftMargin: -Style.marginS * scaling + Layout.alignment: Qt.AlignVCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 500 } + NumberAnimation { to: 0.0; duration: 500 } + } + } + } + + NText { + text: { + if (lockContext.unlockInProgress) return "Authenticating..." + if (lockContext.showFailure && lockContext.errorMessage) return lockContext.errorMessage + if (lockContext.showFailure) return "Authentication failed." + return "" + } + color: { + if (lockContext.unlockInProgress) return Color.mPrimary + if (lockContext.showFailure) return Color.mError + return Color.transparent + } + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeL * scaling + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lockContext.unlockInProgress + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800 } + NumberAnimation { to: 0.5; duration: 800 } + } + } + + Row { + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -10 * scaling + Rectangle { + width: 120 * scaling + height: 40 * scaling + radius: Style.radiusS * scaling + color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS * scaling) + enabled: !lockContext.unlockInProgress + + NText { + anchors.centerIn: parent + text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" + color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + lockContext.tryUnlock() + } + + SequentialAnimation on scale { + running: executeButtonArea.containsMouse + NumberAnimation { to: 1.05; duration: Style.animationFast; easing.type: Easing.OutCubic } + } + + SequentialAnimation on scale { + running: !executeButtonArea.containsMouse + NumberAnimation { to: 1.0; duration: Style.animationFast; easing.type: Easing.OutCubic } + } + } + + SequentialAnimation on scale { + loops: Animation.Infinite + running: lockContext.unlockInProgress + NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } + } + } + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: Color.applyOpacity(Color.mPrimary, "4D") + border.width: Math.max(1, Style.borderS * scaling) + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } + NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } + } + } + } + } + } + + // Power buttons at bottom + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * scaling + spacing: 20 * scaling + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: powerButtonArea.containsMouse ? Color.mError : Color.applyOpacity(Color.mError, "33") + border.color: Color.mError + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "power_settings_new" + font.pointSize: Style.fontSizeXL * scaling + color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError + } + + MouseArea { + id: powerButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.shutdown() + } + } + } + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: restartButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "restart_alt" + font.pointSize: Style.fontSizeXL * scaling + color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + } + + MouseArea { + id: restartButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.reboot() + } + } + } + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: suspendButtonArea.containsMouse ? Color.mSecondary : Color.applyOpacity(Color.mSecondary, "33") + border.color: Color.mSecondary + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "bedtime" + font.pointSize: Style.fontSizeXL * scaling + color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary + } + + MouseArea { + id: suspendButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.suspend() + } + } + } + } } Timer { - interval: 16 // ~60 FPS - running: true - repeat: true - onTriggered: { - waveCanvas.requestPaint() - } - } - } - } - - // Glow effect when no music is playing - Rectangle { - anchors.centerIn: parent - width: parent.width + 24 * scaling - height: parent.height + 24 * scaling - radius: width * 0.5 - color: Color.transparent - border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) - border.width: Math.max(1, Style.borderM * scaling) - z: -1 - visible: !MediaService.isPlaying - - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { - to: 1.1 - duration: 1500 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 1500 - easing.type: Easing.InOutQuad - } - } - } - - NImageCircled { - anchors.centerIn: parent - width: 100 * scaling - height: 100 * scaling - imagePath: Settings.data.general.avatarImage - fallbackIcon: "person" - } - - // Hover animation - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: parent.scale = 1.05 - onExited: parent.scale = 1.0 - } - - Behavior on scale { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutBack - } - } - } - } - } - - // Centered terminal section - Item { - width: 720 * scaling - height: 280 * scaling - anchors.centerIn: parent - - // Futuristic Terminal-Style Input - Item { - width: parent.width - height: 280 * scaling - Layout.fillWidth: true - - // Terminal background with scanlines - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: Style.radiusM * scaling - color: Color.applyOpacity(Color.mSurface, "E6") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - - // Scanline effect - Repeater { - model: 20 - Rectangle { - width: parent.width - height: 1 - color: Color.applyOpacity(Color.mPrimary, "1A") - y: index * 10 * scaling - opacity: Style.opacityMedium - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: 2000 + Math.random() * 1000 - } - NumberAnimation { - to: 0.1 - duration: 2000 + Math.random() * 1000 - } - } - } - } - - // Terminal header - Rectangle { - width: parent.width - height: 40 * scaling - color: Color.applyOpacity(Color.mPrimary, "33") - topLeftRadius: Style.radiusS * scaling - topRightRadius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.topMargin: Style.marginM * scaling - anchors.bottomMargin: Style.marginM * scaling - anchors.leftMargin: Style.marginL * scaling - anchors.rightMargin: Style.marginL * scaling - spacing: Style.marginM * scaling - - NText { - text: "SECURE TERMINAL" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - - // Battery indicator - Row { - spacing: Style.marginS * scaling - visible: batteryIndicator.batteryVisible - - NIcon { - text: batteryIndicator.getIcon() - font.pointSize: Style.fontSizeM * scaling - color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface - } - - NText { - text: Math.round(batteryIndicator.percent) + "%" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - } - - // Keyboard layout indicator - Row { - spacing: Style.marginS * scaling - - NText { - text: keyboardLayout.currentLayout - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - - NIcon { - text: "keyboard_alt" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - } - } - } - } - - // Terminal content area - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Style.marginL * scaling - anchors.topMargin: 70 * scaling - spacing: Style.marginM * scaling - - // Welcome back typing effect - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - id: welcomeText - text: "" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: Style.animationFast + interval: 1000 running: true repeat: true onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } + timeText.text = Qt.formatDateTime(new Date(), "HH:mm") + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") } - } } - } - - // Command line with integrated password input - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - text: "sudo unlock-session" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - } - - // Integrated password input (invisible, just for functionality) - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lock.password - onTextChanged: { - lock.password = text - // Terminal typing sound effect (visual) - typingEffect.start() - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lock.unlockAttempt() - } - } - - Component.onCompleted: { - forceActiveFocus() - } - } - - // Visual password display with integrated cursor - NText { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus - - // Typing effect animation - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 - } - } - } - - // Blinking cursor positioned right after the asterisks - Rectangle { - width: 8 * scaling - height: 20 * scaling - color: Color.mPrimary - visible: passwordInput.activeFocus - Layout.leftMargin: -Style.marginS * scaling - Layout.alignment: Qt.AlignVCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 500 - } - NumberAnimation { - to: 0.0 - duration: 500 - } - } - } - } - - // Status messages - NText { - text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") - color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeL * scaling - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lock.authenticating - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - } - NumberAnimation { - to: 0.5 - duration: 800 - } - } - } - - // Execute button - Row { - Layout.alignment: Qt.AlignRight - Layout.bottomMargin: -10 * scaling - Rectangle { - width: 120 * scaling - height: 40 * scaling - radius: Style.radiusS * scaling - color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS * scaling) - enabled: !lock.authenticating - - NText { - anchors.centerIn: parent - text: lock.authenticating ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: lock.unlockAttempt() - - SequentialAnimation on scale { - running: executeButtonArea.containsMouse - NumberAnimation { - to: 1.05 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - SequentialAnimation on scale { - running: !executeButtonArea.containsMouse - NumberAnimation { - to: 1.0 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - } - - // Processing animation - SequentialAnimation on scale { - loops: Animation.Infinite - running: lock.authenticating - NumberAnimation { - to: 1.02 - duration: 600 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 600 - easing.type: Easing.InOutQuad - } - } - } - } } - - // Terminal glow effect - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Color.transparent - border.color: Color.applyOpacity(Color.mPrimary, "4D") - border.width: Math.max(1, Style.borderS * scaling) - z: -1 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: 2000 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 0.2 - duration: 2000 - easing.type: Easing.InOutQuad - } - } - } - } } - } } - - // Enhanced power buttons with hover effects - Row { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 50 * scaling - spacing: 20 * scaling - - // Shutdown with enhanced styling - Rectangle { - width: 64 * scaling - height: 64 * scaling - radius: Style.radiusL * scaling - color: shutdownArea.containsMouse ? Color.applyOpacity(Color.mError, - "DD") : Color.applyOpacity(Color.mError, "22") - border.color: Color.mError - border.width: Math.max(1, Style.borderM * scaling) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * scaling - height: parent.height + 10 * scaling - radius: width * 0.5 - color: Color.transparent - opacity: shutdownArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - } - - MouseArea { - id: shutdownArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - CompositorService.shutdown() - } - } - - NIcon { - text: "power_settings_new" - font.pointSize: Style.fontSizeXXXL * scaling - color: shutdownArea.containsMouse ? Color.mOnPrimary : Color.mError - anchors.centerIn: parent - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - scale: shutdownArea.containsMouse ? 1.1 : 1.0 - } - - // Reboot with enhanced styling - Rectangle { - width: 64 * scaling - height: 64 * scaling - radius: Style.radiusL * scaling - color: rebootArea.containsMouse ? Color.applyOpacity(Color.mPrimary, - "DD") : Color.applyOpacity(Color.mPrimary, "22") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * scaling - height: parent.height + 10 * scaling - radius: width * 0.5 - color: Color.transparent - opacity: rebootArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { - duration: Style.animationMedium - easing.type: Easing.OutCubic - } - } - } - - MouseArea { - id: rebootArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - CompositorService.reboot() - } - } - - NIcon { - text: "refresh" - font.pointSize: Style.fontSizeXXXL * scaling - color: rebootArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - anchors.centerIn: parent - } - - Behavior on color { - ColorAnimation { - duration: Style.animationMedium - easing.type: Easing.OutCubic - } - } - scale: rebootArea.containsMouse ? 1.1 : 1.0 - } - - // Logout with enhanced styling - Rectangle { - width: 64 * scaling - height: 64 * scaling - radius: Style.radiusL * scaling - color: logoutArea.containsMouse ? Color.applyOpacity(Color.mSecondary, - "DD") : Color.applyOpacity(Color.mSecondary, "22") - border.color: Color.mSecondary - border.width: Math.max(1, Style.borderM * scaling) - - // Glow effect - Rectangle { - anchors.centerIn: parent - width: parent.width + 10 * scaling - height: parent.height + 10 * scaling - radius: width * 0.5 - color: Color.transparent - opacity: logoutArea.containsMouse ? 1 : 0 - z: -1 - - Behavior on opacity { - NumberAnimation { - duration: Style.animationMedium - easing.type: Easing.OutCubic - } - } - } - - MouseArea { - id: logoutArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - CompositorService.logout() - } - } - - NIcon { - text: "exit_to_app" - font.pointSize: Style.fontSizeXXXL * scaling - color: logoutArea.containsMouse ? Color.mOnPrimary : Color.mSecondary - anchors.centerIn: parent - } - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - scale: logoutArea.containsMouse ? 1.1 : 1.0 - } - } - - // Timer for updating time - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - timeText.text = Qt.formatDateTime(new Date(), "HH:mm") - dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") - } - } - } } - } -} +} \ No newline at end of file From 37dad3a255cef383885d95e0558c7957d4e9f57d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 24 Aug 2025 19:38:35 +0200 Subject: [PATCH 48/54] Format --- Modules/LockScreen/LockContext.qml | 152 +-- Modules/LockScreen/LockScreen.qml | 1499 +++++++++++++++------------- Modules/Toast/ToastManager.qml | 4 +- Services/ToastService.qml | 2 +- 4 files changed, 884 insertions(+), 773 deletions(-) diff --git a/Modules/LockScreen/LockContext.qml b/Modules/LockScreen/LockContext.qml index 541fbe7..623ae7b 100644 --- a/Modules/LockScreen/LockContext.qml +++ b/Modules/LockScreen/LockContext.qml @@ -3,90 +3,90 @@ import Quickshell import Quickshell.Services.Pam Scope { - id: root - signal unlocked() - signal failed() + id: root + signal unlocked + signal failed - property string currentText: "" - property bool unlockInProgress: false - property bool showFailure: false - property string errorMessage: "" - property bool pamAvailable: typeof PamContext !== "undefined" + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + property string errorMessage: "" + property bool pamAvailable: typeof PamContext !== "undefined" - onCurrentTextChanged: { - if (currentText !== "") { - showFailure = false; - errorMessage = ""; - } + onCurrentTextChanged: { + if (currentText !== "") { + showFailure = false + errorMessage = "" + } + } + + function tryUnlock() { + if (!pamAvailable) { + errorMessage = "PAM not available" + showFailure = true + return } - function tryUnlock() { - if (!pamAvailable) { - errorMessage = "PAM not available"; - showFailure = true; - return; - } - - if (currentText === "") { - errorMessage = "Password required"; - showFailure = true; - return; - } - - root.unlockInProgress = true; - errorMessage = ""; - showFailure = false; - - console.log("Starting PAM authentication for user:", pam.user); - pam.start(); + if (currentText === "") { + errorMessage = "Password required" + showFailure = true + return } - PamContext { - id: pam - config: "login" - user: Quickshell.env("USER") + root.unlockInProgress = true + errorMessage = "" + showFailure = false - onPamMessage: { - console.log("PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired); - - if (messageIsError) { - errorMessage = message; - } - - if (responseRequired) { - console.log("Responding to PAM with password"); - respond(root.currentText); - } - } + console.log("Starting PAM authentication for user:", pam.user) + pam.start() + } - onResponseRequiredChanged: { - console.log("Response required changed:", responseRequired); - if (responseRequired && root.unlockInProgress) { - console.log("Automatically responding to PAM"); - respond(root.currentText); - } - } + PamContext { + id: pam + config: "login" + user: Quickshell.env("USER") - onCompleted: { - console.log("PAM completed with result:", result); - if (result === PamResult.Success) { - console.log("Authentication successful"); - root.unlocked(); - } else { - console.log("Authentication failed"); - errorMessage = "Authentication failed"; - showFailure = true; - root.failed(); - } - root.unlockInProgress = false; - } + onPamMessage: { + console.log("PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired) - onError: { - console.log("PAM error:", error, "message:", message); - errorMessage = message || "Authentication error"; - showFailure = true; - root.unlockInProgress = false; - root.failed(); - } + if (messageIsError) { + errorMessage = message + } + + if (responseRequired) { + console.log("Responding to PAM with password") + respond(root.currentText) + } } -} \ No newline at end of file + + onResponseRequiredChanged: { + console.log("Response required changed:", responseRequired) + if (responseRequired && root.unlockInProgress) { + console.log("Automatically responding to PAM") + respond(root.currentText) + } + } + + onCompleted: { + console.log("PAM completed with result:", result) + if (result === PamResult.Success) { + console.log("Authentication successful") + root.unlocked() + } else { + console.log("Authentication failed") + errorMessage = "Authentication failed" + showFailure = true + root.failed() + } + root.unlockInProgress = false + } + + onError: { + console.log("PAM error:", error, "message:", message) + errorMessage = message || "Authentication error" + showFailure = true + root.unlockInProgress = false + root.failed() + } + } +} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index de2ea71..f155919 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -14,721 +14,832 @@ import qs.Widgets import qs.Modules.Audio Loader { - id: lockScreen - active: false + id: lockScreen + active: false - Timer { - id: unloadAfterUnlockTimer - interval: 250 - repeat: false - onTriggered: { - lockScreen.active = false + Timer { + id: unloadAfterUnlockTimer + interval: 250 + repeat: false + onTriggered: { + lockScreen.active = false + } + } + + function scheduleUnloadAfterUnlock() { + unloadAfterUnlockTimer.start() + } + + sourceComponent: Component { + Item { + id: lockContainer + + // Create the lock context + LockContext { + id: lockContext + onUnlocked: { + lockSession.locked = false + lockScreen.scheduleUnloadAfterUnlock() + lockContext.currentText = "" } - } + } - function scheduleUnloadAfterUnlock() { - unloadAfterUnlockTimer.start() - } + WlSessionLock { + id: lockSession + locked: lockScreen.active - sourceComponent: Component { - Item { - id: lockContainer + WlSessionLockSurface { + readonly property real scaling: ScalingService.dynamicScale(screen) - // Create the lock context - LockContext { - id: lockContext - onUnlocked: { - lockSession.locked = false - lockScreen.scheduleUnloadAfterUnlock() - lockContext.currentText = "" - } + Item { + id: batteryIndicator + property var battery: UPower.displayDevice + property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent + property real percent: isReady ? (battery.percentage * 100) : 0 + property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false + property bool batteryVisible: isReady && percent > 0 + + function getIcon() { + if (!batteryVisible) + return "" + if (charging) + return "battery_android_bolt" + if (percent >= 95) + return "battery_android_full" + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + } + + Item { + id: keyboardLayout + property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' + && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" + } + + Image { + id: lockBgImage + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" + cache: true + smooth: true + mipmap: false + } + + Rectangle { + anchors.fill: parent + color: Color.transparent + layer.enabled: true + layer.smooth: true + layer.samples: 4 + } + + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.rgba(0, 0, 0, 0.6) + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 0.3) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 0.4) + } + GradientStop { + position: 1.0 + color: Qt.rgba(0, 0, 0, 0.7) + } } - WlSessionLock { - id: lockSession - locked: lockScreen.active + Repeater { + model: 20 + Rectangle { + width: Math.random() * 4 + 2 + height: width + radius: width * 0.5 + color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + x: Math.random() * parent.width + y: Math.random() * parent.height - WlSessionLockSurface { - readonly property real scaling: ScalingService.dynamicScale(screen) + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.8 + duration: 2000 + Math.random() * 3000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 3000 + } + } + } + } + } - Item { - id: batteryIndicator - property var battery: UPower.displayDevice - property bool isReady: battery && battery.ready && battery.isLaptopBattery && battery.isPresent - property real percent: isReady ? (battery.percentage * 100) : 0 - property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false - property bool batteryVisible: isReady && percent > 0 + Item { + anchors.fill: parent - function getIcon() { - if (!batteryVisible) return "" - if (charging) return "battery_android_bolt" - if (percent >= 95) return "battery_android_full" - if (percent >= 85) return "battery_android_6" - if (percent >= 70) return "battery_android_5" - if (percent >= 55) return "battery_android_4" - if (percent >= 40) return "battery_android_3" - if (percent >= 25) return "battery_android_2" - if (percent >= 10) return "battery_android_1" - if (percent >= 0) return "battery_android_0" + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 80 * scaling + spacing: 40 * scaling + + Column { + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignHCenter + + NText { + id: timeText + text: Qt.formatDateTime(new Date(), "HH:mm") + font.family: Settings.data.ui.fontBillboard + font.pointSize: Style.fontSizeXXXL * 6 * scaling + font.weight: Style.fontWeightBold + font.letterSpacing: -2 * scaling + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.02 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + + NText { + id: dateText + text: Qt.formatDateTime(new Date(), "dddd, MMMM d") + font.family: Settings.data.ui.fontBillboard + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Font.Light + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + width: timeText.width + } + } + + Column { + spacing: Style.marginM * scaling + Layout.alignment: Qt.AlignHCenter + + Rectangle { + width: 108 * scaling + height: 108 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderL * scaling) + anchors.horizontalCenter: parent.horizontalCenter + z: 10 + + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Repeater { + model: CavaService.values.length + Rectangle { + property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI + property real linearRadius: 70 * scaling + property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling) + property real linearBarWidth: 3 * scaling + width: linearBarWidth + height: linearBarLength + color: Color.mPrimary + radius: linearBarWidth * 0.5 + x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5 + y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5 + transform: Rotation { + origin.x: linearBarWidth * 0.5 + origin.y: linearBarLength * 0.5 + angle: (linearAngle * 180 / Math.PI) + 90 + } } + } } + } - Item { - id: keyboardLayout - property string currentLayout: (typeof KeyboardLayoutService !== 'undefined' && KeyboardLayoutService.currentLayout) ? KeyboardLayoutService.currentLayout : "Unknown" + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Repeater { + model: CavaService.values.length * 2 + Rectangle { + property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length + * 2 - 1 - index) + property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI + property real mirroredRadius: 70 * scaling + property real mirroredBarLength: Math.max( + 2, CavaService.values[mirroredValueIndex] * 30 * scaling) + property real mirroredBarWidth: 3 * scaling + width: mirroredBarWidth + height: mirroredBarLength + color: Color.mPrimary + radius: mirroredBarWidth * 0.5 + x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5 + y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5 + transform: Rotation { + origin.x: mirroredBarWidth * 0.5 + origin.y: mirroredBarLength * 0.5 + angle: (mirroredAngle * 180 / Math.PI) + 90 + } + } + } } + } - Image { - id: lockBgImage + Loader { + active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave" + anchors.centerIn: parent + width: 160 * scaling + height: 160 * scaling + sourceComponent: Item { + Canvas { + id: waveCanvas anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" - cache: true - smooth: true - mipmap: false - } - - Rectangle { - anchors.fill: parent - color: Color.transparent - layer.enabled: true - layer.smooth: true - layer.samples: 4 - } - - Rectangle { - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.6) } - GradientStop { position: 0.3; color: Qt.rgba(0, 0, 0, 0.3) } - GradientStop { position: 0.7; color: Qt.rgba(0, 0, 0, 0.4) } - GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } + antialiasing: true + onPaint: { + var ctx = getContext("2d") + ctx.reset() + if (CavaService.values.length === 0) + return + ctx.strokeStyle = Color.mPrimary + ctx.lineWidth = 2 * scaling + ctx.lineCap = "round" + var centerX = width * 0.5 + var centerY = height * 0.5 + var baseRadius = 60 * scaling + var maxAmplitude = 20 * scaling + ctx.beginPath() + for (var i = 0; i <= CavaService.values.length; i++) { + var index = i % CavaService.values.length + var angle = (i / CavaService.values.length) * 2 * Math.PI + var amplitude = CavaService.values[index] * maxAmplitude + var radius = baseRadius + amplitude + var x = centerX + Math.cos(angle) * radius + var y = centerY + Math.sin(angle) * radius + if (i === 0) + ctx.moveTo(x, y) + else + ctx.lineTo(x, y) + } + ctx.closePath() + ctx.stroke() } - - Repeater { - model: 20 - Rectangle { - width: Math.random() * 4 + 2 - height: width - radius: width * 0.5 - color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) - x: Math.random() * parent.width - y: Math.random() * parent.height - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.8; duration: 2000 + Math.random() * 3000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 3000 } - } - } - } - } - - Item { - anchors.fill: parent - - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 80 * scaling - spacing: 40 * scaling - - Column { - spacing: Style.marginXS * scaling - Layout.alignment: Qt.AlignHCenter - - NText { - id: timeText - text: Qt.formatDateTime(new Date(), "HH:mm") - font.family: Settings.data.ui.fontBillboard - font.pointSize: Style.fontSizeXXXL * 6 * scaling - font.weight: Style.fontWeightBold - font.letterSpacing: -2 * scaling - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.02; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 2000; easing.type: Easing.InOutQuad } - } - } - - NText { - id: dateText - text: Qt.formatDateTime(new Date(), "dddd, MMMM d") - font.family: Settings.data.ui.fontBillboard - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Font.Light - color: Color.mOnSurface - horizontalAlignment: Text.AlignHCenter - width: timeText.width - } - } - - Column { - spacing: Style.marginM * scaling - Layout.alignment: Qt.AlignHCenter - - Rectangle { - width: 108 * scaling - height: 108 * scaling - radius: width * 0.5 - color: Color.transparent - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderL * scaling) - anchors.horizontalCenter: parent.horizontalCenter - z: 10 - - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "linear" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling - sourceComponent: Item { - Repeater { - model: CavaService.values.length - Rectangle { - property real linearAngle: (index / CavaService.values.length) * 2 * Math.PI - property real linearRadius: 70 * scaling - property real linearBarLength: Math.max(2, CavaService.values[index] * 30 * scaling) - property real linearBarWidth: 3 * scaling - width: linearBarWidth - height: linearBarLength - color: Color.mPrimary - radius: linearBarWidth * 0.5 - x: parent.width * 0.5 + Math.cos(linearAngle) * linearRadius - width * 0.5 - y: parent.height * 0.5 + Math.sin(linearAngle) * linearRadius - height * 0.5 - transform: Rotation { - origin.x: linearBarWidth * 0.5 - origin.y: linearBarLength * 0.5 - angle: (linearAngle * 180 / Math.PI) + 90 - } - } - } - } - } - - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "mirrored" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling - sourceComponent: Item { - Repeater { - model: CavaService.values.length * 2 - Rectangle { - property int mirroredValueIndex: index < CavaService.values.length ? index : (CavaService.values.length * 2 - 1 - index) - property real mirroredAngle: (index / (CavaService.values.length * 2)) * 2 * Math.PI - property real mirroredRadius: 70 * scaling - property real mirroredBarLength: Math.max(2, CavaService.values[mirroredValueIndex] * 30 * scaling) - property real mirroredBarWidth: 3 * scaling - width: mirroredBarWidth - height: mirroredBarLength - color: Color.mPrimary - radius: mirroredBarWidth * 0.5 - x: parent.width * 0.5 + Math.cos(mirroredAngle) * mirroredRadius - width * 0.5 - y: parent.height * 0.5 + Math.sin(mirroredAngle) * mirroredRadius - height * 0.5 - transform: Rotation { - origin.x: mirroredBarWidth * 0.5 - origin.y: mirroredBarLength * 0.5 - angle: (mirroredAngle * 180 / Math.PI) + 90 - } - } - } - } - } - - Loader { - active: MediaService.isPlaying && Settings.data.audio.visualizerType == "wave" - anchors.centerIn: parent - width: 160 * scaling - height: 160 * scaling - sourceComponent: Item { - Canvas { - id: waveCanvas - anchors.fill: parent - antialiasing: true - onPaint: { - var ctx = getContext("2d") - ctx.reset() - if (CavaService.values.length === 0) return - ctx.strokeStyle = Color.mPrimary - ctx.lineWidth = 2 * scaling - ctx.lineCap = "round" - var centerX = width * 0.5 - var centerY = height * 0.5 - var baseRadius = 60 * scaling - var maxAmplitude = 20 * scaling - ctx.beginPath() - for (var i = 0; i <= CavaService.values.length; i++) { - var index = i % CavaService.values.length - var angle = (i / CavaService.values.length) * 2 * Math.PI - var amplitude = CavaService.values[index] * maxAmplitude - var radius = baseRadius + amplitude - var x = centerX + Math.cos(angle) * radius - var y = centerY + Math.sin(angle) * radius - if (i === 0) ctx.moveTo(x, y) - else ctx.lineTo(x, y) - } - ctx.closePath() - ctx.stroke() - } - } - Timer { - interval: 16 - running: true - repeat: true - onTriggered: waveCanvas.requestPaint() - } - } - } - - Rectangle { - anchors.centerIn: parent - width: parent.width + 24 * scaling - height: parent.height + 24 * scaling - radius: width * 0.5 - color: Color.transparent - border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) - border.width: Math.max(1, Style.borderM * scaling) - z: -1 - visible: !MediaService.isPlaying - SequentialAnimation on scale { - loops: Animation.Infinite - NumberAnimation { to: 1.1; duration: 1500; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 1500; easing.type: Easing.InOutQuad } - } - } - - NImageCircled { - anchors.centerIn: parent - width: 100 * scaling - height: 100 * scaling - imagePath: Settings.data.general.avatarImage - fallbackIcon: "person" - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: parent.scale = 1.05 - onExited: parent.scale = 1.0 - } - - Behavior on scale { - NumberAnimation { duration: Style.animationFast; easing.type: Easing.OutBack } - } - } - } - } - - Item { - width: 720 * scaling - height: 280 * scaling - anchors.centerIn: parent - anchors.verticalCenterOffset: 50 * scaling - - Item { - width: parent.width - height: 280 * scaling - Layout.fillWidth: true - - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: Style.radiusM * scaling - color: Color.applyOpacity(Color.mSurface, "E6") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - - Repeater { - model: 20 - Rectangle { - width: parent.width - height: 1 - color: Color.applyOpacity(Color.mPrimary, "1A") - y: index * 10 * scaling - opacity: Style.opacityMedium - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000 + Math.random() * 1000 } - NumberAnimation { to: 0.1; duration: 2000 + Math.random() * 1000 } - } - } - } - - Rectangle { - width: parent.width - height: 40 * scaling - color: Color.applyOpacity(Color.mPrimary, "33") - topLeftRadius: Style.radiusS * scaling - topRightRadius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.topMargin: Style.marginM * scaling - anchors.bottomMargin: Style.marginM * scaling - anchors.leftMargin: Style.marginL * scaling - anchors.rightMargin: Style.marginL * scaling - spacing: Style.marginM * scaling - - NText { - text: "SECURE TERMINAL" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - - Row { - spacing: Style.marginS * scaling - visible: batteryIndicator.batteryVisible - NIcon { - text: batteryIndicator.getIcon() - font.pointSize: Style.fontSizeM * scaling - color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface - } - NText { - text: Math.round(batteryIndicator.percent) + "%" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - } - - Row { - spacing: Style.marginS * scaling - NText { - text: keyboardLayout.currentLayout - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - NIcon { - text: "keyboard_alt" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - } - } - } - } - - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Style.marginL * scaling - anchors.topMargin: 70 * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - id: welcomeText - text: "" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: Style.animationFast - running: true - repeat: true - onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - text: "sudo unlock-session" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - } - - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lockContext.currentText - onTextChanged: { - lockContext.currentText = text - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lockContext.tryUnlock() - } - } - - Component.onCompleted: { - forceActiveFocus() - } - } - - NText { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus - - SequentialAnimation { - id: typingEffect - NumberAnimation { target: passwordInput; property: "scale"; to: 1.01; duration: 50 } - NumberAnimation { target: passwordInput; property: "scale"; to: 1.0; duration: 50 } - } - } - - Rectangle { - width: 8 * scaling - height: 20 * scaling - color: Color.mPrimary - visible: passwordInput.activeFocus - Layout.leftMargin: -Style.marginS * scaling - Layout.alignment: Qt.AlignVCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 500 } - NumberAnimation { to: 0.0; duration: 500 } - } - } - } - - NText { - text: { - if (lockContext.unlockInProgress) return "Authenticating..." - if (lockContext.showFailure && lockContext.errorMessage) return lockContext.errorMessage - if (lockContext.showFailure) return "Authentication failed." - return "" - } - color: { - if (lockContext.unlockInProgress) return Color.mPrimary - if (lockContext.showFailure) return Color.mError - return Color.transparent - } - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeL * scaling - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lockContext.unlockInProgress - loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 800 } - NumberAnimation { to: 0.5; duration: 800 } - } - } - - Row { - Layout.alignment: Qt.AlignRight - Layout.bottomMargin: -10 * scaling - Rectangle { - width: 120 * scaling - height: 40 * scaling - radius: Style.radiusS * scaling - color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS * scaling) - enabled: !lockContext.unlockInProgress - - NText { - anchors.centerIn: parent - text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - lockContext.tryUnlock() - } - - SequentialAnimation on scale { - running: executeButtonArea.containsMouse - NumberAnimation { to: 1.05; duration: Style.animationFast; easing.type: Easing.OutCubic } - } - - SequentialAnimation on scale { - running: !executeButtonArea.containsMouse - NumberAnimation { to: 1.0; duration: Style.animationFast; easing.type: Easing.OutCubic } - } - } - - SequentialAnimation on scale { - loops: Animation.Infinite - running: lockContext.unlockInProgress - NumberAnimation { to: 1.02; duration: 600; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } - } - } - } - } - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Color.transparent - border.color: Color.applyOpacity(Color.mPrimary, "4D") - border.width: Math.max(1, Style.borderS * scaling) - z: -1 - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: 2000; easing.type: Easing.InOutQuad } - NumberAnimation { to: 0.2; duration: 2000; easing.type: Easing.InOutQuad } - } - } - } - } - } - - // Power buttons at bottom - Row { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 50 * scaling - spacing: 20 * scaling - - Rectangle { - width: 60 * scaling - height: 60 * scaling - radius: width * 0.5 - color: powerButtonArea.containsMouse ? Color.mError : Color.applyOpacity(Color.mError, "33") - border.color: Color.mError - border.width: Math.max(1, Style.borderM * scaling) - - NIcon { - anchors.centerIn: parent - text: "power_settings_new" - font.pointSize: Style.fontSizeXL * scaling - color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError - } - - MouseArea { - id: powerButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - CompositorService.shutdown() - } - } - } - - Rectangle { - width: 60 * scaling - height: 60 * scaling - radius: width * 0.5 - color: restartButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - - NIcon { - anchors.centerIn: parent - text: "restart_alt" - font.pointSize: Style.fontSizeXL * scaling - color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - } - - MouseArea { - id: restartButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - CompositorService.reboot() - } - } - } - - Rectangle { - width: 60 * scaling - height: 60 * scaling - radius: width * 0.5 - color: suspendButtonArea.containsMouse ? Color.mSecondary : Color.applyOpacity(Color.mSecondary, "33") - border.color: Color.mSecondary - border.width: Math.max(1, Style.borderM * scaling) - - NIcon { - anchors.centerIn: parent - text: "bedtime" - font.pointSize: Style.fontSizeXL * scaling - color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary - } - - MouseArea { - id: suspendButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - CompositorService.suspend() - } - } - } - } - } - - Timer { - interval: 1000 + } + Timer { + interval: 16 running: true repeat: true - onTriggered: { - timeText.text = Qt.formatDateTime(new Date(), "HH:mm") - dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") - } + onTriggered: waveCanvas.requestPaint() + } } + } + + Rectangle { + anchors.centerIn: parent + width: parent.width + 24 * scaling + height: parent.height + 24 * scaling + radius: width * 0.5 + color: Color.transparent + border.color: Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.3) + border.width: Math.max(1, Style.borderM * scaling) + z: -1 + visible: !MediaService.isPlaying + SequentialAnimation on scale { + loops: Animation.Infinite + NumberAnimation { + to: 1.1 + duration: 1500 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 1500 + easing.type: Easing.InOutQuad + } + } + } + + NImageCircled { + anchors.centerIn: parent + width: 100 * scaling + height: 100 * scaling + imagePath: Settings.data.general.avatarImage + fallbackIcon: "person" + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: parent.scale = 1.05 + onExited: parent.scale = 1.0 + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + } + } } + } } + + Item { + width: 720 * scaling + height: 280 * scaling + anchors.centerIn: parent + anchors.verticalCenterOffset: 50 * scaling + + Item { + width: parent.width + height: 280 * scaling + Layout.fillWidth: true + + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: Style.radiusM * scaling + color: Color.applyOpacity(Color.mSurface, "E6") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + + Repeater { + model: 20 + Rectangle { + width: parent.width + height: 1 + color: Color.applyOpacity(Color.mPrimary, "1A") + y: index * 10 * scaling + opacity: Style.opacityMedium + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + Math.random() * 1000 + } + NumberAnimation { + to: 0.1 + duration: 2000 + Math.random() * 1000 + } + } + } + } + + Rectangle { + width: parent.width + height: 40 * scaling + color: Color.applyOpacity(Color.mPrimary, "33") + topLeftRadius: Style.radiusS * scaling + topRightRadius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.topMargin: Style.marginM * scaling + anchors.bottomMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginL * scaling + anchors.rightMargin: Style.marginL * scaling + spacing: Style.marginM * scaling + + NText { + text: "SECURE TERMINAL" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + Row { + spacing: Style.marginS * scaling + visible: batteryIndicator.batteryVisible + NIcon { + text: batteryIndicator.getIcon() + font.pointSize: Style.fontSizeM * scaling + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + } + + Row { + spacing: Style.marginS * scaling + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + NIcon { + text: "keyboard_alt" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + } + } + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Style.marginL * scaling + anchors.topMargin: 70 * scaling + spacing: Style.marginM * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + id: welcomeText + text: "" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + text: "sudo unlock-session" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + } + + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lockContext.currentText + onTextChanged: { + lockContext.currentText = text + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lockContext.tryUnlock() + } + } + + Component.onCompleted: { + forceActiveFocus() + } + } + + NText { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + visible: passwordInput.activeFocus + + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + Layout.leftMargin: -Style.marginS * scaling + Layout.alignment: Qt.AlignVCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } + } + } + } + + NText { + text: { + if (lockContext.unlockInProgress) + return "Authenticating..." + if (lockContext.showFailure && lockContext.errorMessage) + return lockContext.errorMessage + if (lockContext.showFailure) + return "Authentication failed." + return "" + } + color: { + if (lockContext.unlockInProgress) + return Color.mPrimary + if (lockContext.showFailure) + return Color.mError + return Color.transparent + } + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeL * scaling + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lockContext.unlockInProgress + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + } + NumberAnimation { + to: 0.5 + duration: 800 + } + } + } + + Row { + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -10 * scaling + Rectangle { + width: 120 * scaling + height: 40 * scaling + radius: Style.radiusS * scaling + color: executeButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, + "33") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS * scaling) + enabled: !lockContext.unlockInProgress + + NText { + anchors.centerIn: parent + text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" + color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + lockContext.tryUnlock() + } + + SequentialAnimation on scale { + running: executeButtonArea.containsMouse + NumberAnimation { + to: 1.05 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + SequentialAnimation on scale { + running: !executeButtonArea.containsMouse + NumberAnimation { + to: 1.0 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + SequentialAnimation on scale { + loops: Animation.Infinite + running: lockContext.unlockInProgress + NumberAnimation { + to: 1.02 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + } + } + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: Color.applyOpacity(Color.mPrimary, "4D") + border.width: Math.max(1, Style.borderS * scaling) + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 0.2 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + } + } + } + + // Power buttons at bottom + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 50 * scaling + spacing: 20 * scaling + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: powerButtonArea.containsMouse ? Color.mError : Color.applyOpacity(Color.mError, "33") + border.color: Color.mError + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "power_settings_new" + font.pointSize: Style.fontSizeXL * scaling + color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError + } + + MouseArea { + id: powerButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.shutdown() + } + } + } + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: restartButtonArea.containsMouse ? Color.mPrimary : Color.applyOpacity(Color.mPrimary, "33") + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "restart_alt" + font.pointSize: Style.fontSizeXL * scaling + color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + } + + MouseArea { + id: restartButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.reboot() + } + } + } + + Rectangle { + width: 60 * scaling + height: 60 * scaling + radius: width * 0.5 + color: suspendButtonArea.containsMouse ? Color.mSecondary : Color.applyOpacity(Color.mSecondary, "33") + border.color: Color.mSecondary + border.width: Math.max(1, Style.borderM * scaling) + + NIcon { + anchors.centerIn: parent + text: "bedtime" + font.pointSize: Style.fontSizeXL * scaling + color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary + } + + MouseArea { + id: suspendButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + CompositorService.suspend() + } + } + } + } + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + timeText.text = Qt.formatDateTime(new Date(), "HH:mm") + dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d") + } + } } + } } -} \ No newline at end of file + } +} diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml index 8bb8706..0da45c1 100644 --- a/Modules/Toast/ToastManager.qml +++ b/Modules/Toast/ToastManager.qml @@ -19,7 +19,7 @@ Variants { // Only show on screens that have notifications enabled visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false + || (Settings.data.notifications.monitors.length === 0)) : false // Position based on bar location, like Notification popup does anchors { @@ -57,7 +57,7 @@ Variants { Component.onCompleted: { // Only register toasts for screens that have notifications enabled if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name) - || (Settings.data.notifications.monitors.length === 0)) : false) { + || (Settings.data.notifications.monitors.length === 0)) : false) { // Register this toast with the service ToastService.allToasts.push(toast) diff --git a/Services/ToastService.qml b/Services/ToastService.qml index 38ce2db..5db7139 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -211,7 +211,7 @@ Singleton { break } } - + if (allDismissed) { isShowingToast = false From a110a0d6366f19fcbef2e925e13de07990849ecb Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 15:33:34 -0400 Subject: [PATCH 49/54] LockContext: logging use Logger + fix missing result argument --- Modules/LockScreen/LockContext.qml | 40 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Modules/LockScreen/LockContext.qml b/Modules/LockScreen/LockContext.qml index 623ae7b..985bcd4 100644 --- a/Modules/LockScreen/LockContext.qml +++ b/Modules/LockScreen/LockContext.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Services.Pam +import qs.Commons Scope { id: root @@ -37,7 +38,7 @@ Scope { errorMessage = "" showFailure = false - console.log("Starting PAM authentication for user:", pam.user) + Logger.log("LockContext", "Starting PAM authentication for user:", pam.user) pam.start() } @@ -47,42 +48,43 @@ Scope { user: Quickshell.env("USER") onPamMessage: { - console.log("PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired) + Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", + responseRequired) if (messageIsError) { errorMessage = message } if (responseRequired) { - console.log("Responding to PAM with password") + Logger.log("LockContext", "Responding to PAM with password") respond(root.currentText) } } onResponseRequiredChanged: { - console.log("Response required changed:", responseRequired) + Logger.log("LockContext", "Response required changed:", responseRequired) if (responseRequired && root.unlockInProgress) { - console.log("Automatically responding to PAM") + Logger.log("LockContext", "Automatically responding to PAM") respond(root.currentText) } } - onCompleted: { - console.log("PAM completed with result:", result) - if (result === PamResult.Success) { - console.log("Authentication successful") - root.unlocked() - } else { - console.log("Authentication failed") - errorMessage = "Authentication failed" - showFailure = true - root.failed() - } - root.unlockInProgress = false - } + onCompleted: result => { + Logger.log("LockContext", "PAM completed with result:", result) + if (result === PamResult.Success) { + Logger.log("LockContext", "Authentication successful") + root.unlocked() + } else { + Logger.log("LockContext", "Authentication failed") + errorMessage = "Authentication failed" + showFailure = true + root.failed() + } + root.unlockInProgress = false + } onError: { - console.log("PAM error:", error, "message:", message) + Logger.log("LockContext", "PAM error:", error, "message:", message) errorMessage = message || "Authentication error" showFailure = true root.unlockInProgress = false From a10d55e7f5106508c81b13eb26b52340df4b2412 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 23:50:09 -0400 Subject: [PATCH 50/54] Bar widgets: modular loading refactoring via BarWidgetRegistry+NWidgetLoader - Hot reload is working again. - Should also be more memory efficient on multi monitors. --- Commons/WidgetLoader.qml | 88 ---------- Modules/Bar/Bar.qml | 89 ++-------- Modules/Bar/{Widgets => Extras}/TrayMenu.qml | 0 Modules/Bar/Widgets/ActiveWindow.qml | 9 +- Modules/Bar/Widgets/ArchUpdater.qml | 16 +- Modules/Bar/Widgets/Battery.qml | 155 ++++++++++-------- Modules/Bar/Widgets/Bluetooth.qml | 9 +- Modules/Bar/Widgets/Brightness.qml | 9 +- Modules/Bar/Widgets/Clock.qml | 15 +- Modules/Bar/Widgets/KeyboardLayout.qml | 9 +- Modules/Bar/Widgets/MediaMini.qml | 4 + Modules/Bar/Widgets/NotificationHistory.qml | 8 +- Modules/Bar/Widgets/PowerProfile.qml | 4 +- .../Bar/Widgets/ScreenRecorderIndicator.qml | 13 +- Modules/Bar/Widgets/SidePanelToggle.qml | 9 +- Modules/Bar/Widgets/SystemMonitor.qml | 4 + Modules/Bar/Widgets/Tray.qml | 15 +- Modules/Bar/Widgets/Volume.qml | 8 +- Modules/Bar/Widgets/WiFi.qml | 9 +- Modules/Bar/Widgets/Workspace.qml | 64 ++++---- Modules/Calendar/Calendar.qml | 11 -- Modules/SettingsPanel/Tabs/BarTab.qml | 41 ++--- Modules/SidePanel/Cards/UtilitiesCard.qml | 1 + Services/BarWidgetRegistry.qml | 99 +++++++++++ Services/CavaService.qml | 2 +- Services/CompositorService.qml | 2 +- Services/GitHubService.qml | 3 +- Services/LocationService.qml | 3 +- Services/NotificationService.qml | 3 +- Services/PanelService.qml | 31 +++- Services/ScreenRecorderService.qml | 1 - Widgets/NPanel.qml | 8 +- Widgets/NPill.qml | 30 ++-- .../{NWidgetCard.qml => NSectionEditor.qml} | 51 +++--- Widgets/NWidgetLoader.qml | 46 ++++++ shell.qml | 91 +++++----- 36 files changed, 514 insertions(+), 446 deletions(-) delete mode 100644 Commons/WidgetLoader.qml rename Modules/Bar/{Widgets => Extras}/TrayMenu.qml (100%) create mode 100644 Services/BarWidgetRegistry.qml rename Widgets/{NWidgetCard.qml => NSectionEditor.qml} (83%) create mode 100644 Widgets/NWidgetLoader.qml diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml deleted file mode 100644 index 0a2b01c..0000000 --- a/Commons/WidgetLoader.qml +++ /dev/null @@ -1,88 +0,0 @@ -import QtQuick -import qs.Commons - -QtObject { - id: root - - // Signal emitted when widget loading status changes - signal widgetLoaded(string widgetName) - signal widgetFailed(string widgetName, string error) - signal loadingComplete(int total, int loaded, int failed) - - // Properties to track loading status - property int totalWidgets: 0 - property int loadedWidgets: 0 - property int failedWidgets: 0 - - // Auto-discover widget components - function getWidgetComponent(widgetName) { - if (!widgetName || widgetName.trim() === "") { - return null - } - - const widgetPath = `../Modules/Bar/Widgets/${widgetName}.qml` - - // Try to load the widget directly from file - const component = Qt.createComponent(widgetPath) - if (component.status === Component.Ready) { - return component - } - - const errorMsg = `Failed to load ${widgetName}.qml widget, status: ${component.status}, error: ${component.errorString( - )}` - Logger.error("WidgetLoader", errorMsg) - return null - } - - // Initialize loading tracking - function initializeLoading(widgetList) { - totalWidgets = widgetList.length - loadedWidgets = 0 - failedWidgets = 0 - } - - // Track widget loading success - function onWidgetLoaded(widgetName) { - loadedWidgets++ - widgetLoaded(widgetName) - - if (loadedWidgets + failedWidgets === totalWidgets) { - Logger.log("WidgetLoader", `Loaded ${loadedWidgets} widgets`) - loadingComplete(totalWidgets, loadedWidgets, failedWidgets) - } - } - - // Track widget loading failure - function onWidgetFailed(widgetName, error) { - failedWidgets++ - widgetFailed(widgetName, error) - - if (loadedWidgets + failedWidgets === totalWidgets) { - loadingComplete(totalWidgets, loadedWidgets, failedWidgets) - } - } - - // This is where you should add your Modules/Bar/Widgets/ - // so it gets registered in the BarTab - function discoverAvailableWidgets() { - const widgetFiles = ["ActiveWindow", "ArchUpdater", "Battery", "Bluetooth", "Brightness", "Clock", "KeyboardLayout", "MediaMini", "NotificationHistory", "PowerProfile", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"] - - const availableWidgets = [] - - widgetFiles.forEach(widgetName => { - // Test if the widget can be loaded - const component = getWidgetComponent(widgetName) - if (component) { - availableWidgets.push({ - "key": widgetName, - "name": widgetName - }) - } - }) - - // Sort alphabetically - availableWidgets.sort((a, b) => a.name.localeCompare(b.name)) - - return availableWidgets - } -} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 48ed76e..eccb872 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -48,6 +48,7 @@ Variants { layer.enabled: true } + // ------------------------------ // Left Section - Dynamic Widgets Row { id: leftSection @@ -61,30 +62,19 @@ Variants { Repeater { model: Settings.data.bar.widgets.left delegate: Loader { - id: leftWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - if (modelData === "Battery" && !shouldShowBattery()) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } + // ------------------------------ // Center Section - Dynamic Widgets Row { id: centerSection @@ -97,30 +87,19 @@ Variants { Repeater { model: Settings.data.bar.widgets.center delegate: Loader { - id: centerWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - if (modelData === "Battery" && !shouldShowBattery()) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } + // ------------------------------ // Right Section - Dynamic Widgets Row { id: rightSection @@ -134,49 +113,17 @@ Variants { Repeater { model: Settings.data.bar.widgets.right delegate: Loader { - id: rightWidgetLoader - sourceComponent: widgetLoader.getWidgetComponent(modelData) active: true - visible: { - if (modelData === "WiFi" && !Settings.data.network.wifiEnabled) - return false - if (modelData === "Bluetooth" && !Settings.data.network.bluetoothEnabled) - return false - return true - } - anchors.verticalCenter: parent.verticalCenter - onStatusChanged: { - if (status === Loader.Error) { - widgetLoader.onWidgetFailed(modelData, "Loader error") - } else if (status === Loader.Ready) { - widgetLoader.onWidgetLoaded(modelData) + sourceComponent: NWidgetLoader { + widgetName: modelData + widgetProps: { + "screen": screen } } + anchors.verticalCenter: parent.verticalCenter } } } } - - // Helper function to check if battery widget should be visible (same logic as Battery.qml) - function shouldShowBattery() { - // For now, always show battery widget and let it handle its own visibility - // The Battery widget has its own testMode and visibility logic - return true - } - - // Widget loader instance - WidgetLoader { - id: widgetLoader - - onWidgetFailed: function (widgetName, error) { - Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`) - } - } - - // Initialize widget loading tracking - Component.onCompleted: { - const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right] - widgetLoader.initializeLoading(allWidgets) - } } } diff --git a/Modules/Bar/Widgets/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml similarity index 100% rename from Modules/Bar/Widgets/TrayMenu.qml rename to Modules/Bar/Extras/TrayMenu.qml diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 904b5b4..8e41fcc 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -9,13 +9,16 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + property bool showingFullTitle: false + property int lastWindowIndex: -1 + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: getTitle() !== "" - property bool showingFullTitle: false - property int lastWindowIndex: -1 - // Timer to hide full title after window switch Timer { id: fullTitleTimer diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 6b09817..4636840 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -1,16 +1,18 @@ -import qs.Commons -import qs.Services -import qs.Widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets NIconButton { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + sizeMultiplier: 0.8 - - readonly property real scaling: ScalingService.scale(screen) - colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -64,7 +66,7 @@ NIconButton { if (ArchUpdaterService.updatePackages.length > 0) { // Show confirmation dialog for updates - PanelService.updatePanel.toggle(screen) + PanelService.getPanel("archUpdaterPanel").toggle(screen) } else { // Just refresh if no updates available ArchUpdaterService.doPoll() diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 8349da5..46f5701 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -6,89 +6,100 @@ import qs.Commons import qs.Services import qs.Widgets -NPill { +Item { id: root - // Test mode - property bool testMode: false - property int testPercent: 49 - property bool testCharging: false + property ShellScreen screen + property real scaling: ScalingService.scale(screen) - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - property bool show: isReady && percent > 0 + implicitWidth: pill.width + implicitHeight: pill.height - // Choose icon based on charge and charging state - function batteryIcon() { + NPill { + id: pill - if (charging) - return "battery_android_bolt" + // Test mode + property bool testMode: false + property int testPercent: 49 + property bool testCharging: false - if (percent >= 95) - return "battery_android_full" + property var battery: UPower.displayDevice + property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) + property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6" - if (percent >= 70) - return "battery_android_5" - if (percent >= 55) - return "battery_android_4" - if (percent >= 40) - return "battery_android_3" - if (percent >= 25) - return "battery_android_2" - if (percent >= 10) - return "battery_android_1" - if (percent >= 0) - return "battery_android_0" - } + // Choose icon based on charge and charging state + function batteryIcon() { - visible: testMode || (isReady && battery.isLaptopBattery) + if (!isReady || !battery.isLaptopBattery) + return "battery_android_alert" - icon: root.batteryIcon() - text: Math.round(root.percent) + "%" - textColor: charging ? Color.mPrimary : Color.mOnSurface - forceShown: Settings.data.bar.alwaysShowBatteryPercentage - tooltipText: { - let lines = [] + if (charging) + return "battery_android_bolt" - if (testMode) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) + if (percent >= 95) + return "battery_android_full" + + // Hardcoded battery symbols + if (percent >= 85) + return "battery_android_6" + if (percent >= 70) + return "battery_android_5" + if (percent >= 55) + return "battery_android_4" + if (percent >= 40) + return "battery_android_3" + if (percent >= 25) + return "battery_android_2" + if (percent >= 10) + return "battery_android_1" + if (percent >= 0) + return "battery_android_0" + } + + icon: batteryIcon() + text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-" + textColor: charging ? Color.mPrimary : Color.mOnSurface + forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage + disableOpen: (!isReady || !battery.isLaptopBattery) + tooltipText: { + let lines = [] + + if (testMode) { + lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345)) + return lines.join("\n") + } + + if (!isReady || !battery.isLaptopBattery) { + return "No Battery Detected" + } + + if (battery.timeToEmpty > 0) { + lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty)) + } + + if (battery.timeToFull > 0) { + lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull)) + } + + if (battery.changeRate !== undefined) { + const rate = battery.changeRate + if (rate > 0) { + lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed( + 2) + " W") + } else if (rate < 0) { + lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W") + } else { + lines.push("Estimating...") + } + } else { + lines.push(charging ? "Charging" : "Discharging") + } + + if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) { + lines.push("Health: " + Math.round(battery.healthPercentage) + "%") + } return lines.join("\n") } - - if (!root.isReady) { - return "" - } - - if (root.battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty)) - } - - if (root.battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull)) - } - - if (root.battery.changeRate !== undefined) { - const rate = root.battery.changeRate - if (rate > 0) { - lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( - 2) + " W") - } else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") - } else { - lines.push("Estimating...") - } - } else { - lines.push(root.charging ? "Charging" : "Discharging") - } - - if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) { - lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%") - } - return lines.join("\n") } } diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index bbe35f1..f977128 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -10,8 +10,11 @@ import qs.Widgets NIconButton { id: root - sizeMultiplier: 0.8 + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + visible: Settings.data.network.bluetoothEnabled + sizeMultiplier: 0.8 colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -28,7 +31,5 @@ NIconButton { } } tooltipText: "Bluetooth Devices" - onClicked: { - bluetoothPanel.toggle(screen) - } + onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 58c8cc6..5091e66 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -8,13 +8,16 @@ import qs.Widgets Item { id: root - width: pill.width - height: pill.height - visible: getMonitor() !== null + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false + width: pill.width + height: pill.height + visible: getMonitor() !== null + function getMonitor() { return BrightnessService.getMonitorForScreen(screen) || null } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 1a269c9..ced71d8 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -1,16 +1,21 @@ import QtQuick +import Quickshell import qs.Commons import qs.Services import qs.Widgets -// Clock Icon with attached calendar Rectangle { id: root - width: clock.width + Style.marginM * 2 * scaling - height: Math.round(Style.capsuleHeight * scaling) + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + implicitWidth: clock.width + Style.marginM * 2 * scaling + implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant + // Clock Icon with attached calendar NClock { id: clock anchors.verticalCenter: parent.verticalCenter @@ -24,7 +29,7 @@ Rectangle { } onEntered: { - if (!calendarPanel.active) { + if (!PanelService.getPanel("calendarPanel")?.active) { tooltip.show() } } @@ -33,7 +38,7 @@ Rectangle { } onClicked: { tooltip.hide() - calendarPanel.toggle(screen) + PanelService.getPanel("calendarPanel")?.toggle(screen) } } } diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 1ae3acb..0ee56e5 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -6,15 +6,18 @@ import qs.Commons import qs.Services import qs.Widgets -Item { +Row { id: root - width: pill.width - height: pill.height + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout + width: pill.width + height: pill.height + NPill { id: pill icon: "keyboard_alt" diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index b884196..dee6c42 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -9,6 +9,10 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 8cf0502..15adeea 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -10,6 +10,9 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + sizeMultiplier: 0.8 icon: "notifications" tooltipText: "Notification History" @@ -17,8 +20,5 @@ NIconButton { colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - - onClicked: { - notificationHistoryPanel.toggle(screen) - } + onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index 7d3afa9..47fab43 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -1,7 +1,7 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Services.UPower -import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets @@ -9,6 +9,8 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) property var powerProfiles: PowerProfiles readonly property bool hasPP: powerProfiles.hasPerformanceProfile diff --git a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml index f7606c9..2df0ef3 100644 --- a/Modules/Bar/Widgets/ScreenRecorderIndicator.qml +++ b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml @@ -1,18 +1,21 @@ +import Quickshell import qs.Commons import qs.Services import qs.Widgets // Screen Recording Indicator NIconButton { - id: screenRecordingIndicator + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + visible: ScreenRecorderService.isRecording icon: "videocam" tooltipText: "Screen Recording Active\nClick To Stop Recording" sizeMultiplier: 0.8 colorBg: Color.mPrimary colorFg: Color.mOnPrimary - visible: ScreenRecorderService.isRecording anchors.verticalCenter: parent.verticalCenter - onClicked: { - ScreenRecorderService.toggleRecording() - } + onClicked: ScreenRecorderService.toggleRecording() } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 42c634c..5b985b8 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -1,9 +1,14 @@ import Quickshell import qs.Commons import qs.Widgets +import qs.Services NIconButton { - id: sidePanelToggle + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + icon: "widgets" tooltipText: "Open Side Panel" sizeMultiplier: 0.8 @@ -14,5 +19,5 @@ NIconButton { colorBorderHover: Color.transparent anchors.verticalCenter: parent.verticalCenter - onClicked: sidePanel.toggle(screen) + onClicked: PanelService.getPanel("sidePanel")?.toggle(screen) } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 18bb8b6..610a940 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -6,6 +6,10 @@ import qs.Widgets Row { id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 553e56b..3d97c85 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -6,15 +6,20 @@ import Quickshell import Quickshell.Services.SystemTray import Quickshell.Widgets import qs.Commons +import qs.Modules.Bar.Extras import qs.Services import qs.Widgets Rectangle { + id: root + + property ShellScreen screen + property real scaling: ScalingService.scale(screen) readonly property real itemSize: 24 * scaling visible: SystemTray.items.values.length > 0 - width: tray.width + Style.marginM * scaling * 2 - height: Math.round(Style.capsuleHeight * scaling) + implicitWidth: tray.width + Style.marginM * scaling * 2 + implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant @@ -134,9 +139,7 @@ Rectangle { function open() { visible = true - // Register into the panel service - // so this will autoclose if we open another panel - PanelService.registerOpen(trayPanel) + PanelService.willOpenPanel(trayPanel) } function close() { @@ -152,7 +155,7 @@ Rectangle { Loader { id: trayMenu - source: "TrayMenu.qml" + source: "../Extras/TrayMenu.qml" } } } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 36dbbb1..e115102 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -9,12 +9,15 @@ import qs.Widgets Item { id: root - width: pill.width - height: pill.height + property ShellScreen screen + property real scaling: ScalingService.scale(screen) // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false + implicitWidth: pill.width + implicitHeight: pill.height + function getIcon() { if (AudioService.muted) { return "volume_off" @@ -64,6 +67,7 @@ Item { } } onClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.AudioService settingsPanel.open(screen) } diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 5b94a38..845a110 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -10,6 +10,11 @@ import qs.Widgets NIconButton { id: root + property ShellScreen screen + property real scaling: ScalingService.scale(screen) + + visible: Settings.data.network.wifiEnabled + sizeMultiplier: 0.8 Component.onCompleted: { @@ -44,11 +49,11 @@ NIconButton { return "signal_wifi_bad" } } - tooltipText: "WiFi Networks" + tooltipText: "Network / WiFi" onClicked: { try { Logger.log("WiFi", "Button clicked, toggling panel") - wifiPanel.toggle(screen) + PanelService.getPanel("wifiPanel")?.toggle(screen) } catch (error) { Logger.error("WiFi", "Error toggling panel:", error) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 220a6b3..0871ce7 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -10,6 +10,10 @@ import qs.Services Item { id: root + + property ShellScreen screen: null + property real scaling: ScalingService.scale(screen) + property bool isDestroying: false property bool hovered: false @@ -23,7 +27,8 @@ Item { signal workspaceChanged(int workspaceId, color accentColor) - width: { + implicitHeight: Math.round(36 * scaling) + implicitWidth: { let total = 0 for (var i = 0; i < localWorkspaces.count; i++) { const ws = localWorkspaces.get(i) @@ -39,34 +44,35 @@ Item { return total } - height: Math.round(36 * scaling) - Component.onCompleted: { - localWorkspaces.clear() - for (var i = 0; i < WorkspaceService.workspaces.count; i++) { - const ws = WorkspaceService.workspaces.get(i) - if (ws.output.toLowerCase() === screen.name.toLowerCase()) { - localWorkspaces.append(ws) - } - } - workspaceRepeater.model = localWorkspaces - updateWorkspaceFocus() + refreshWorkspaces() } + Component.onDestruction: { + root.isDestroying = true + } + + onScreenChanged: refreshWorkspaces() + Connections { target: WorkspaceService function onWorkspacesChanged() { - localWorkspaces.clear() + refreshWorkspaces() + } + } + + function refreshWorkspaces() { + localWorkspaces.clear() + if (screen !== null) { for (var i = 0; i < WorkspaceService.workspaces.count; i++) { const ws = WorkspaceService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { localWorkspaces.append(ws) } } - - workspaceRepeater.model = localWorkspaces - updateWorkspaceFocus() } + workspaceRepeater.model = localWorkspaces + updateWorkspaceFocus() } function triggerUnifiedWave() { @@ -74,6 +80,17 @@ Item { masterAnimation.restart() } + function updateWorkspaceFocus() { + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i) + if (ws.isFocused === true) { + root.triggerUnifiedWave() + root.workspaceChanged(ws.id, Color.mPrimary) + break + } + } + } + SequentialAnimation { id: masterAnimation PropertyAction { @@ -101,17 +118,6 @@ Item { } } - function updateWorkspaceFocus() { - for (var i = 0; i < localWorkspaces.count; i++) { - const ws = localWorkspaces.get(i) - if (ws.isFocused === true) { - root.triggerUnifiedWave() - root.workspaceChanged(ws.id, Color.mPrimary) - break - } - } - } - Rectangle { id: workspaceBackground width: parent.width - Style.marginS * scaling * 2 @@ -254,8 +260,4 @@ Item { } } } - - Component.onDestruction: { - root.isDestroying = true - } } diff --git a/Modules/Calendar/Calendar.qml b/Modules/Calendar/Calendar.qml index 762c8ff..ea5b260 100644 --- a/Modules/Calendar/Calendar.qml +++ b/Modules/Calendar/Calendar.qml @@ -104,17 +104,6 @@ NPanel { year: Time.date.getFullYear() locale: Qt.locale() // Use system locale - // Optionally, update when the panel becomes visible - Connections { - target: calendarPanel - function onVisibleChanged() { - if (calendarPanel.visible) { - grid.month = Time.date.getMonth() - grid.year = Time.date.getFullYear() - } - } - } - delegate: Rectangle { width: (Style.baseWidgetSize * scaling) height: (Style.baseWidgetSize * scaling) diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index d0476c6..efee796 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -163,7 +163,7 @@ ColumnLayout { spacing: Style.marginM * scaling // Left Section - NWidgetCard { + NSectionEditor { sectionName: "Left" widgetModel: Settings.data.bar.widgets.left availableWidgets: availableWidgets @@ -174,7 +174,7 @@ ColumnLayout { } // Center Section - NWidgetCard { + NSectionEditor { sectionName: "Center" widgetModel: Settings.data.bar.widgets.center availableWidgets: availableWidgets @@ -185,7 +185,7 @@ ColumnLayout { } // Right Section - NWidgetCard { + NSectionEditor { sectionName: "Right" widgetModel: Settings.data.bar.widgets.right availableWidgets: availableWidgets @@ -228,15 +228,6 @@ ColumnLayout { // Assign the new array Settings.data.bar.widgets[section] = newArray - - // Force a settings save - //Logger.log("BarTab", "Settings updated, triggering save...") - - // Verify the change was applied - Qt.setTimeout(function () { - var updatedArray = Settings.data.bar.widgets[section] - //Logger.log("BarTab", "Verification - updated section array:", JSON.stringify(updatedArray)) - }, 100) } else { //Logger.log("BarTab", "Invalid section or index:", section, index, "array length:", @@ -262,29 +253,19 @@ ColumnLayout { } } - // Widget loader for discovering available widgets - WidgetLoader { - id: widgetLoader - } - + // Base list model for all combo boxes ListModel { id: availableWidgets } Component.onCompleted: { - discoverWidgets() - } - - // Automatically discover available widgets using WidgetLoader - function discoverWidgets() { + // Fill out availableWidgets ListModel availableWidgets.clear() - - // Use WidgetLoader to discover available widgets - const discoveredWidgets = widgetLoader.discoverAvailableWidgets() - - // Add discovered widgets to the ListModel - discoveredWidgets.forEach(widget => { - availableWidgets.append(widget) - }) + BarWidgetRegistry.getAvailableWidgets().forEach(entry => { + availableWidgets.append({ + "key": entry, + "name": entry + }) + }) } } diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index 8d3b67a..26f3b4e 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -50,6 +50,7 @@ NBox { icon: "image" tooltipText: "Open Wallpaper Selector" onClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.open(screen) } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml new file mode 100644 index 0000000..b6e735c --- /dev/null +++ b/Services/BarWidgetRegistry.qml @@ -0,0 +1,99 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Modules.Bar.Widgets + +Singleton { + id: root + + // Widget registry object mapping widget names to components + property var widgets: ({ + "ActiveWindow": activeWindowComponent, + "ArchUpdater": archUpdaterComponent, + "Battery": batteryComponent, + "Bluetooth": bluetoothComponent, + "Brightness": brightnessComponent, + "Clock": clockComponent, + "KeyboardLayout": keyboardLayoutComponent, + "MediaMini": mediaMiniComponent, + "NotificationHistory": notificationHistoryComponent, + "PowerProfile": powerProfileComponent, + "ScreenRecorderIndicator": screenRecorderIndicatorComponent, + "SidePanelToggle": sidePanelToggleComponent, + "SystemMonitor": systemMonitorComponent, + "Tray": trayComponent, + "Volume": volumeComponent, + "WiFi": wiFiComponent, + "Workspace": workspaceComponent + }) + + // Component definitions - these are loaded once at startup + property Component activeWindowComponent: Component { + ActiveWindow {} + } + property Component archUpdaterComponent: Component { + ArchUpdater {} + } + property Component batteryComponent: Component { + Battery {} + } + property Component bluetoothComponent: Component { + Bluetooth {} + } + property Component brightnessComponent: Component { + Brightness {} + } + property Component clockComponent: Component { + Clock {} + } + property Component keyboardLayoutComponent: Component { + KeyboardLayout {} + } + property Component mediaMiniComponent: Component { + MediaMini {} + } + property Component notificationHistoryComponent: Component { + NotificationHistory {} + } + property Component powerProfileComponent: Component { + PowerProfile {} + } + property Component screenRecorderIndicatorComponent: Component { + ScreenRecorderIndicator {} + } + property Component sidePanelToggleComponent: Component { + SidePanelToggle {} + } + property Component systemMonitorComponent: Component { + SystemMonitor {} + } + property Component trayComponent: Component { + Tray {} + } + property Component volumeComponent: Component { + Volume {} + } + property Component wiFiComponent: Component { + WiFi {} + } + property Component workspaceComponent: Component { + Workspace {} + } + + // ------------------------------ + // Helper function to get widget component by name + function getWidget(name) { + return widgets[name] || null + } + + // Helper function to check if widget exists + function hasWidget(name) { + return name in widgets + } + + // Get list of available widget names + function getAvailableWidgets() { + return Object.keys(widgets) + } +} diff --git a/Services/CavaService.qml b/Services/CavaService.qml index cb7b603..6cfb735 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -38,7 +38,7 @@ Singleton { id: process stdinEnabled: true running: (Settings.data.audio.visualizerType !== "none") - && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava + && (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava || (PanelService.lockScreen && PanelService.lockScreen.active)) command: ["cava", "-p", "/dev/stdin"] onExited: { diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index a1ec62b..feb52bc 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -19,7 +19,7 @@ Singleton { property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 - property string focusedWindowTitle: "(No active window)" + property string focusedWindowTitle: "n/a" property bool inOverview: false // Generic events diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 98bf6e4..cb75563 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -1,9 +1,10 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services -pragma Singleton // GitHub API logic and caching Singleton { diff --git a/Services/LocationService.qml b/Services/LocationService.qml index c758856..2e29e88 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -1,9 +1,10 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services -pragma Singleton // Weather logic and caching Singleton { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 3f4f44e..0354df5 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -1,10 +1,11 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services import Quickshell.Services.Notifications -pragma Singleton QtObject { id: root diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 435a45a..9a37aeb 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -1,23 +1,38 @@ pragma Singleton import Quickshell +import qs.Commons Singleton { id: root - // A ref. to the sidePanel, so it's accessible from other services - property var sidePanel: null - - // A ref. to the lockScreen, so it's accessible from other services + // A ref. to the lockScreen, so it's accessible from anywhere + // This is not a panel... property var lockScreen: null - // A ref. to the updatePanel, so it's accessible from other services - property var updatePanel: null - // Currently opened panel property var openedPanel: null - function registerOpen(panel) { + property var registeredPanels: ({}) + + // Register this panel + function registerPanel(panel) { + registeredPanels[panel.objectName] = panel + Logger.log("PanelService", "Registered:", panel.objectName) + } + + // Returns a panel + function getPanel(name) { + return registeredPanels[name] || null + } + + // Check if a panel exists + function hasPanel(name) { + return name in registeredPanels + } + + // Helper to keep only one panel open at any time + function willOpenPanel(panel) { if (openedPanel && openedPanel != panel) { openedPanel.close() } diff --git a/Services/ScreenRecorderService.qml b/Services/ScreenRecorderService.qml index 5837314..ed35b38 100644 --- a/Services/ScreenRecorderService.qml +++ b/Services/ScreenRecorderService.qml @@ -46,7 +46,6 @@ Singleton { //Logger.log("ScreenRecorder", command) Quickshell.execDetached(["sh", "-c", command]) Logger.log("ScreenRecorder", "Started recording") - //Logger.log("ScreenRecorder", command) } // Stop recording using Quickshell.execDetached diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 97a8705..dc8e162 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -31,6 +31,12 @@ Loader { signal opened signal closed + Component.onCompleted: { + // console.log("Oh Yeah") + // console.log(objectName) + PanelService.registerPanel(root) + } + // ----------------------------------------- function toggle(aScreen) { if (!active || isClosing) { @@ -53,7 +59,7 @@ Loader { opacityValue = 1.0 } - PanelService.registerOpen(root) + PanelService.willOpenPanel(root) active = true root.opened() diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index b3739eb..8f6c3d4 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -16,10 +16,11 @@ Item { property color collapsedIconColor: Color.mOnSurface property real sizeMultiplier: 0.8 property bool autoHide: false - // When true, keep the pill expanded regardless of hover state - property bool forceShown: false + property bool forceOpen: false + property bool disableOpen: false + // Effective shown state (true if hovered/animated open or forced) - readonly property bool effectiveShown: forceShown || showPill + readonly property bool effectiveShown: forceOpen || showPill signal shown signal hidden @@ -85,7 +86,7 @@ Item { height: iconSize radius: width * 0.5 // When forced shown, match pill background; otherwise use accent when hovered - color: forceShown ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) + color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -100,7 +101,7 @@ Item { text: root.icon font.pointSize: Style.fontSizeM * scaling // When forced shown, use pill text color; otherwise accent color when hovered - color: forceShown ? textColor : (showPill ? iconTextColor : Color.mOnSurface) + color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface) anchors.centerIn: parent } } @@ -194,18 +195,21 @@ Item { anchors.fill: parent hoverEnabled: true onEntered: { - if (!forceShown) { + root.entered() + tooltip.show() + if (disableOpen) { + return + } + if (!forceOpen) { showDelayed() } - tooltip.show() - root.entered() } onExited: { - if (!forceShown) { + root.exited() + if (!forceOpen) { hide() } tooltip.hide() - root.exited() } onClicked: { root.clicked() @@ -226,7 +230,7 @@ Item { } function hide() { - if (forceShown) { + if (forceOpen) { return } if (showPill) { @@ -245,8 +249,8 @@ Item { } } - onForceShownChanged: { - if (forceShown) { + onForceOpenChanged: { + if (forceOpen) { // Immediately lock open without animations showAnim.stop() hideAnim.stop() diff --git a/Widgets/NWidgetCard.qml b/Widgets/NSectionEditor.qml similarity index 83% rename from Widgets/NWidgetCard.qml rename to Widgets/NSectionEditor.qml index abf53c7..9103dcd 100644 --- a/Widgets/NWidgetCard.qml +++ b/Widgets/NSectionEditor.qml @@ -90,6 +90,7 @@ NBox { colorFgHover: Color.mOnSecondary enabled: comboBox.selectedKey !== "" Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling onClicked: { if (comboBox.currentKey !== "") { addWidget(comboBox.currentKey, sectionName.toLowerCase()) @@ -174,27 +175,27 @@ NBox { anchors.fill: parent drag.target: parent - onPressed: { - // Check if the click is on the close button area - const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling - const closeButtonY = widgetContent.y - const closeButtonWidth = 20 * scaling - const closeButtonHeight = 20 * scaling + onPressed: mouse => { + // Check if the click is on the close button area + const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling + const closeButtonY = widgetContent.y + const closeButtonWidth = 20 * scaling + const closeButtonHeight = 20 * scaling - if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth && mouseY >= closeButtonY - && mouseY <= closeButtonY + closeButtonHeight) { - // Click is on the close button, don't start drag - mouse.accepted = false - return - } + if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth + && mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) { + // Click is on the close button, don't start drag + mouse.accepted = false + return + } - Logger.log("NWidgetCard", `Started dragging widget: ${modelData} at index ${index}`) - // Bring to front when starting drag - widgetItem.z = 1000 - } + Logger.log("NSectionEditor", `Started dragging widget: ${modelData} at index ${index}`) + // Bring to front when starting drag + widgetItem.z = 1000 + } onReleased: { - Logger.log("NWidgetCard", `Released widget: ${modelData} at index ${index}`) + Logger.log("NSectionEditor", `Released widget: ${modelData} at index ${index}`) // Reset z-index when drag ends widgetItem.z = 0 @@ -232,12 +233,12 @@ NBox { const fromIndex = index const toIndex = targetIndex Logger.log( - "NWidgetCard", + "NSectionEditor", `Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed( 2)})`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } else { - Logger.log("NWidgetCard", `No valid drop target found for widget at index ${index}`) + Logger.log("NSectionEditor", `No valid drop target found for widget at index ${index}`) } } } @@ -264,16 +265,16 @@ NBox { } onEntered: function (drag) { - Logger.log("NWidgetCard", "Entered start drop zone") + Logger.log("NSectionEditor", "Entered start drop zone") } onDropped: function (drop) { - Logger.log("NWidgetCard", "Dropped on start zone") + Logger.log("NSectionEditor", "Dropped on start zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex const toIndex = 0 // Insert at the beginning if (fromIndex !== toIndex) { - Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to beginning`) + Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to beginning`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } } @@ -299,16 +300,16 @@ NBox { } onEntered: function (drag) { - Logger.log("NWidgetCard", "Entered end drop zone") + Logger.log("NSectionEditor", "Entered end drop zone") } onDropped: function (drop) { - Logger.log("NWidgetCard", "Dropped on end zone") + Logger.log("NSectionEditor", "Dropped on end zone") if (drop.source && drop.source.widgetIndex !== undefined) { const fromIndex = drop.source.widgetIndex const toIndex = widgetModel.length // Insert at the end if (fromIndex !== toIndex) { - Logger.log("NWidgetCard", `Dropped widget from index ${fromIndex} to end`) + Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to end`) reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex) } } diff --git a/Widgets/NWidgetLoader.qml b/Widgets/NWidgetLoader.qml new file mode 100644 index 0000000..600b5e7 --- /dev/null +++ b/Widgets/NWidgetLoader.qml @@ -0,0 +1,46 @@ +import QtQuick +import Quickshell +import qs.Services + +Item { + id: root + + property string widgetName: "" + property var widgetProps: ({}) + property bool enabled: true + + // Don't reserve space unless the loaded widget is really visible + implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0 + implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0 + + Loader { + id: loader + + anchors.fill: parent + active: enabled && widgetName !== "" + sourceComponent: { + if (!active) { + return null + } + return BarWidgetRegistry.getWidget(widgetName) + } + + onLoaded: { + if (item && widgetProps) { + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] + } + } + } + } + } + + // Error handling + onWidgetNameChanged: { + if (widgetName && !BarWidgetRegistry.hasWidget(widgetName)) { + Logger.warn("WidgetLoader", "Widget not found in registry:", widgetName) + } + } +} diff --git a/shell.qml b/shell.qml index 6eafab7..370b788 100644 --- a/shell.qml +++ b/shell.qml @@ -40,64 +40,69 @@ ShellRoot { Bar {} Dock {} - Launcher { - id: launcherPanel - } - - SidePanel { - id: sidePanel - } - - Calendar { - id: calendarPanel - } - - SettingsPanel { - id: settingsPanel - } - Notification { id: notification } - NotificationHistoryPanel { - id: notificationHistoryPanel - } - LockScreen { id: lockScreen } - PowerPanel { - id: powerPanel - } - - WiFiPanel { - id: wifiPanel - } - - BluetoothPanel { - id: bluetoothPanel - } - - ArchUpdaterPanel { - id: updatePanel - } - ToastManager {} IPCManager {} + // ------------------------------ + // All the panels + Launcher { + id: launcherPanel + objectName: "launcherPanel" + } + + SidePanel { + id: sidePanel + objectName: "sidePanel" + } + + Calendar { + id: calendarPanel + objectName: "calendarPanel" + } + + SettingsPanel { + id: settingsPanel + objectName: "settingsPanel" + } + + NotificationHistoryPanel { + id: notificationHistoryPanel + objectName: "notificationHistoryPanel" + } + + PowerPanel { + id: powerPanel + objectName: "powerPanel" + } + + WiFiPanel { + id: wifiPanel + objectName: "wifiPanel" + } + + BluetoothPanel { + id: bluetoothPanel + objectName: "bluetoothPanel" + } + + ArchUpdaterPanel { + id: archUpdaterPanel + objectName: "archUpdaterPanel" + } + Component.onCompleted: { - // Save a ref. to our sidePanel so we can access it from services - PanelService.sidePanel = sidePanel - - // Save a ref. to our lockScreen so we can access it from services + // Save a ref. to our lockScreen so we can access it easily PanelService.lockScreen = lockScreen - // Save a ref. to our updatePanel so we can access it from services - PanelService.updatePanel = updatePanel - // Ensure our singleton is created as soon as possible so we start fetching weather asap LocationService.init() } From d7206410f4f14d33c541e6a5218477df8a005435 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 24 Aug 2025 23:59:44 -0400 Subject: [PATCH 51/54] Disabled arch updater until more investiguation --- Services/BarWidgetRegistry.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index b6e735c..62ea73c 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -10,7 +10,7 @@ Singleton { // Widget registry object mapping widget names to components property var widgets: ({ "ActiveWindow": activeWindowComponent, - "ArchUpdater": archUpdaterComponent, + // "ArchUpdater": archUpdaterComponent, "Battery": batteryComponent, "Bluetooth": bluetoothComponent, "Brightness": brightnessComponent, @@ -32,9 +32,9 @@ Singleton { property Component activeWindowComponent: Component { ActiveWindow {} } - property Component archUpdaterComponent: Component { - ArchUpdater {} - } + // property Component archUpdaterComponent: Component { + // ArchUpdater {} + // } property Component batteryComponent: Component { Battery {} } From bf16a6ee16d7c96fc288893467368af1bcd59af9 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 25 Aug 2025 00:01:33 -0400 Subject: [PATCH 52/54] Bar: fixed brightness widget size --- Modules/Bar/Widgets/Brightness.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 5091e66..432a859 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -14,8 +14,8 @@ Item { // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false - width: pill.width - height: pill.height + implicitWidth: pill.width + implicitHeight: pill.height visible: getMonitor() !== null function getMonitor() { From 4fcc6b84553a09ac00b0dcd2c123a127c3437d81 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 25 Aug 2025 06:22:31 +0200 Subject: [PATCH 53/54] Fixx Launcher warning --- Modules/Launcher/Launcher.qml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 65fb759..99d79cb 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -26,15 +26,13 @@ NPanel { // Properties property string searchText: "" + property bool shouldResetCursor: false // Add function to set search text programmatically function setSearchText(text) { searchText = text - if (searchInput) { - searchInput.text = text - searchInput.cursorPosition = text.length - searchInput.forceActiveFocus() - } + // The searchInput will automatically update via the text binding + // Focus and cursor position will be handled by the TextField's Component.onCompleted } onOpened: { @@ -43,19 +41,13 @@ NPanel { searchText = "" selectedIndex = 0 } - if (searchInput) { - searchInput.forceActiveFocus() - } } onClosed: { // Reset search bar when launcher is closed searchText = "" selectedIndex = 0 - if (searchInput) { - searchInput.text = "" - searchInput.cursorPosition = 0 - } + shouldResetCursor = true } // Import modular components @@ -283,6 +275,12 @@ NPanel { } // Defer selectedIndex reset to avoid binding loops Qt.callLater(() => selectedIndex = 0) + + // Reset cursor position if needed + if (shouldResetCursor && text === "") { + cursorPosition = 0 + shouldResetCursor = false + } } selectedTextColor: Color.mOnSurface selectionColor: Color.mPrimary @@ -293,10 +291,14 @@ NPanel { topPadding: 0 bottomPadding: 0 Component.onCompleted: { - // Focus the search bar by default + // Focus the search bar by default and set cursor position Qt.callLater(() => { selectedIndex = 0 searchInput.forceActiveFocus() + // Set cursor to end if there's already text + if (searchText && searchText.length > 0) { + searchInput.cursorPosition = searchText.length + } }) } Keys.onDownPressed: selectNext() From ee13e5eaa8bd3500c4a5185d01c36020e975747d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 25 Aug 2025 06:27:22 +0200 Subject: [PATCH 54/54] Add SidePanel IPC toggle --- Modules/IPC/IPCManager.qml | 7 +++++++ README.md | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index ad63934..3e26628 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -98,4 +98,11 @@ Item { powerPanel.toggle(Quickshell.screens[0]) } } + + IpcHandler { + target: "sidePanel" + function toggle() { + sidePanel.toggle(Quickshell.screens[0]) + } + } } diff --git a/README.md b/README.md index 60a09e9..1901027 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,9 @@ qs # Launcher qs ipc call launcher toggle +# SidePanel +qs ipc call sidePanel toggle + # Clipboard History qs ipc call launcher clipboard