From 299add4a157fac900a7b612c6b59a88baf489778 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 30 Aug 2025 02:28:48 +0200 Subject: [PATCH] Remove need for polkit, launch any ArchUpdater update through terminal ArchUpdater: rely on `TERMINAL` environment variable README: Add explanation for the `TERMINAL` environment variable --- Commons/Logger.qml | 24 - Commons/Settings.qml | 57 +- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 225 +++++++- Modules/Background/Background.qml | 162 +----- Modules/Background/Overview.qml | 29 +- Modules/Bar/Widgets/ArchUpdater.qml | 1 - Modules/LockScreen/LockScreen.qml | 2 +- Modules/SettingsPanel/Tabs/DisplayTab.qml | 16 +- .../Tabs/WallpaperSelectorTab.qml | 115 ++-- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 517 +++++++++++++----- README.md | 30 +- Services/ArchUpdaterService.qml | 463 ++++++++++++---- Services/MatugenService.qml | 4 +- Services/ScalingService.qml | 37 +- Services/WallpaperService.qml | 334 ++++------- Widgets/NLabel.qml | 8 +- Widgets/NTextInput.qml | 4 - Widgets/NWidgetLoader.qml | 2 +- flake.nix | 1 + 19 files changed, 1232 insertions(+), 799 deletions(-) diff --git a/Commons/Logger.qml b/Commons/Logger.qml index 72b62e4..22a4726 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -17,14 +17,6 @@ Singleton { } } - function _getStackTrace() { - try { - throw new Error("Stack trace") - } catch (e) { - return e.stack - } - } - function log(...args) { var msg = _formatMessage(...args) console.log(msg) @@ -39,20 +31,4 @@ Singleton { var msg = _formatMessage(...args) console.error(msg) } - - function callStack() { - var stack = _getStackTrace() - Logger.log("Debug", "--------------------------") - Logger.log("Debug", "Current call stack") - // Split the stack into lines and log each one - var stackLines = stack.split('\n') - for (var i = 0; i < stackLines.length; i++) { - var line = stackLines[i].trim() // Remove leading/trailing whitespace - if (line.length > 0) { - // Only log non-empty lines - Logger.log("Debug", `- ${line}`) - } - } - Logger.log("Debug", "--------------------------") - } } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index b2759fb..17187be 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -89,16 +89,23 @@ Singleton { reload() } onLoaded: function () { - if (!isLoaded) { - Logger.log("Settings", "----------------------------") - Logger.log("Settings", "Settings loaded successfully") - isLoaded = true + Qt.callLater(function () { + // Some stuff like wallpaper setup and settings validation should just be executed once on startup + // And not on every reload + if (!isLoaded) { + Logger.log("Settings", "JSON completed loading") + if (adapter.wallpaper.current !== "") { + Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) + WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) + } - Qt.callLater(function () { - // Some stuff like settings validation should just be executed once on startup and not on every reload + // Validate monitor configurations, only once + // if none of the configured monitors exist, clear the lists validateMonitorConfigurations() - }) - } + + isLoaded = true + } + }) } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) @@ -164,13 +171,22 @@ Singleton { // wallpaper property JsonObject wallpaper: JsonObject { property string directory: "/usr/share/wallpapers" - property bool enableMultiMonitorDirectories: false - property bool setWallpaperOnAllMonitors: true - property bool randomEnabled: false - property int randomIntervalSec: 300 // 5 min - property int transitionDuration: 1500 // 1500 ms - property string transitionType: "fade" - property list monitors: [] + 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 @@ -218,10 +234,19 @@ Singleton { 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 - property list monitorsScaling: [] + + // 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 diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index a4759ca..d1e2474 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -15,8 +15,9 @@ NPanel { // When the panel opens onOpened: { - ArchUpdaterService.doPoll() - ArchUpdaterService.doAurPoll() + console.log("ArchUpdaterPanel: Panel opened, refreshing package lists...") + // Always refresh when panel opens to ensure we have the latest data + ArchUpdaterService.forceRefresh() } panelContent: Rectangle { @@ -47,6 +48,19 @@ NPanel { Layout.fillWidth: true } + // Reset button (only show if update failed) + NIconButton { + visible: ArchUpdaterService.updateFailed + icon: "refresh" + tooltipText: "Reset update state" + sizeRatio: 0.8 + colorBg: Color.mError + colorFg: Color.mOnError + onClicked: { + ArchUpdaterService.resetUpdateState() + } + } + NIconButton { icon: "close" tooltipText: "Close" @@ -59,8 +73,10 @@ NPanel { Layout.fillWidth: true } - // Update summary + // Update summary (only show when packages are available) NText { + visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy + && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightMedium @@ -68,16 +84,184 @@ NPanel { Layout.fillWidth: true } - // Package selection info + // Package selection info (only show when not updating and have packages) NText { + visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy + && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected" font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurfaceVariant Layout.fillWidth: true } - // Unified list + // Update in progress state + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + visible: ArchUpdaterService.updateInProgress + spacing: Style.marginM * scaling + + Item { + Layout.fillHeight: true + } // Spacer + + NIcon { + text: "hourglass_empty" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Update in progress" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Please check your terminal window for update progress and prompts." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + Layout.maximumWidth: 280 * scaling + } + + Item { + Layout.fillHeight: true + } // Spacer + } + + // Update failed state + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: ArchUpdaterService.updateFailed + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM * scaling + + NIcon { + text: "error_outline" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mError + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Update failed" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Check your terminal for error details and try again." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + Layout.maximumWidth: 280 * scaling + } + + // Prominent refresh button + NIconButton { + icon: "refresh" + tooltipText: "Refresh and try again" + sizeRatio: 1.2 + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + onClicked: { + ArchUpdaterService.resetUpdateState() + } + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Style.marginL * scaling + } + } + } + + // No updates available state + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy + && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates === 0 + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM * scaling + + NIcon { + text: "check_circle" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "System is up to date" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "All packages are current. Check back later for updates." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + Layout.maximumWidth: 280 * scaling + } + } + } + + // Checking for updates state + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) && !ArchUpdaterService.updateInProgress + && !ArchUpdaterService.updateFailed + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM * scaling + + NIcon { + text: "refresh" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Checking for updates" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning package databases for available updates..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + Layout.maximumWidth: 280 * scaling + } + } + } + + // Package list (only show when not in any special state) NBox { + visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed && !ArchUpdaterService.busy + && !ArchUpdaterService.aurBusy && ArchUpdaterService.totalUpdates > 0 Layout.fillWidth: true Layout.fillHeight: true @@ -164,50 +348,49 @@ NPanel { } } - // Action buttons + // Action buttons (only show when not updating) RowLayout { + visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed Layout.fillWidth: true spacing: Style.marginL * scaling NIconButton { icon: "refresh" - tooltipText: "Check for updates" + tooltipText: "Refresh package lists" onClicked: { - ArchUpdaterService.doPoll() - ArchUpdaterService.doAurPoll() + ArchUpdaterService.forceRefresh() } colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface Layout.fillWidth: true + enabled: !ArchUpdaterService.busy && !ArchUpdaterService.aurBusy } NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt" - tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" - enabled: !ArchUpdaterService.updateInProgress + icon: "system_update_alt" + tooltipText: "Update all packages" + enabled: ArchUpdaterService.totalUpdates > 0 onClicked: { ArchUpdaterService.runUpdate() root.close() } - colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary - colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary + colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant + colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant Layout.fillWidth: true } NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box" - tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" - enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 + icon: "check_box" + tooltipText: "Update selected packages" + enabled: ArchUpdaterService.selectedPackagesCount > 0 onClicked: { if (ArchUpdaterService.selectedPackagesCount > 0) { ArchUpdaterService.runSelectiveUpdate() root.close() } } - colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount - > 0 ? Color.mPrimary : Color.mSurfaceVariant) - colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount - > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant) + colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant + colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant Layout.fillWidth: true } } diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 9d8a5f9..54a7878 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -4,67 +4,27 @@ import Quickshell.Wayland import qs.Commons import qs.Services -Variants { - id: backgroundVariants - model: Quickshell.screens +Loader { + active: !Settings.data.wallpaper.swww.enabled - delegate: Loader { + sourceComponent: Variants { + model: Quickshell.screens - required property ShellScreen modelData + delegate: PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" - active: Settings.isLoaded && WallpaperService.getWallpaper(modelData.name) + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled - sourceComponent: PanelWindow { - id: root + // Force update when SWWW setting changes + onVisibleChanged: { + if (visible) { - // Internal state management - property bool firstWallpaper: true - property bool transitioning: false - property real transitionProgress: 0.0 + } else { - // Wipe direction: 0=left, 1=right, 2=up, 3=down - property real wipeDirection: 0 - property real wipeSmoothness: 0.05 - - // External state management - property string servicedWallpaper: WallpaperService.getWallpaper(modelData.name) - onServicedWallpaperChanged: { - if (servicedWallpaper && servicedWallpaper !== currentWallpaper.source) { - - // Set wallpaper immediately on startup - if (firstWallpaper) { - firstWallpaper = false - setWallpaperImmediate(servicedWallpaper) - return - } - - switch (Settings.data.wallpaper.transitionType) { - case "none": - setWallpaperImmediate(servicedWallpaper) - break - case "wipe_left": - wipeDirection = 0 - setWallpaperWithTransition(servicedWallpaper) - break - case "wipe_right": - wipeDirection = 1 - setWallpaperWithTransition(servicedWallpaper) - break - case "wipe_up": - wipeDirection = 2 - setWallpaperWithTransition(servicedWallpaper) - break - case "wipe_down": - wipeDirection = 3 - setWallpaperWithTransition(servicedWallpaper) - break - default: - setWallpaperWithTransition(servicedWallpaper) - break - } } } - color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background @@ -78,106 +38,18 @@ Variants { left: true } - Image { - id: currentWallpaper - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - source: "" - cache: true - smooth: true - mipmap: false - visible: false + margins { + top: 0 } Image { - id: nextWallpaper anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: "" + source: wallpaperSource + visible: wallpaperSource !== "" cache: true smooth: true mipmap: false - visible: false - } - - // Fade transition shader - ShaderEffect { - id: fadeShader - anchors.fill: parent - visible: Settings.data.wallpaper.transitionType === 'fade' - - property variant source1: currentWallpaper - property variant source2: nextWallpaper - property real fade: transitionProgress - fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb") - } - - // Wipe transition shader - ShaderEffect { - id: wipeShader - anchors.fill: parent - visible: Settings.data.wallpaper.transitionType.startsWith('wipe_') - - property variant source1: currentWallpaper - property variant source2: nextWallpaper - property real progress: transitionProgress - property real direction: wipeDirection - property real smoothness: wipeSmoothness - fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb") - } - - // Animation for the transition progress - NumberAnimation { - id: transitionAnimation - target: root - property: "transitionProgress" - from: 0.0 - to: 1.0 - duration: Settings.data.wallpaper.transitionDuration ?? 1000 - easing.type: { - const transitionType = Settings.data.wallpaper.transitionType ?? 'fade' - if (transitionType.startsWith('wipe_')) { - return Easing.InOutCubic - } - return Easing.InOutCubic - } - - onFinished: { - // Swap images after transition completes - currentWallpaper.source = nextWallpaper.source - transitionProgress = 0.0 - transitioning = false - } - } - - function startTransition() { - if (!transitioning && nextWallpaper.source != currentWallpaper.source) { - transitioning = true - transitionAnimation.start() - } - } - - function setWallpaperImmediate(source) { - currentWallpaper.source = source - nextWallpaper.source = source - transitionProgress = 0.0 - transitioning = false - } - - function setWallpaperWithTransition(source) { - if (source != currentWallpaper.source) { - - if (transitioning) { - // We are interrupting a transition - currentWallpaper.source = nextWallpaper.source - transitionAnimation.stop() - transitionProgress = 0 - transitioning = false - } - - nextWallpaper.source = source - startTransition() - } } } } diff --git a/Modules/Background/Overview.qml b/Modules/Background/Overview.qml index 32237d1..e673663 100644 --- a/Modules/Background/Overview.qml +++ b/Modules/Background/Overview.qml @@ -6,19 +6,24 @@ import qs.Commons import qs.Services import qs.Widgets -Variants { - model: Quickshell.screens +Loader { + active: CompositorService.isNiri - delegate: Loader { - required property ShellScreen modelData + Component.onCompleted: { + if (CompositorService.isNiri) { + Logger.log("Overview", "Loading Overview component for Niri") + } + } - active: Settings.isLoaded && CompositorService.isNiri + sourceComponent: Variants { + model: Quickshell.screens - sourceComponent: PanelWindow { - Component.onCompleted: { - Logger.log("Overview", "Loading Overview component for Niri on", modelData.name) - } + delegate: PanelWindow { + required property ShellScreen modelData + property string wallpaperSource: WallpaperService.currentWallpaper !== "" + && !Settings.data.wallpaper.swww.enabled ? WallpaperService.currentWallpaper : "" + visible: wallpaperSource !== "" && !Settings.data.wallpaper.swww.enabled color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background @@ -34,15 +39,19 @@ Variants { Image { id: bgImage + anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpaperService.getWallpaper(modelData.name) + source: wallpaperSource cache: true smooth: true mipmap: false + visible: wallpaperSource !== "" } MultiEffect { + id: overviewBgBlur + anchors.fill: parent source: bgImage blurEnabled: true diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 106b167..05598e1 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -64,6 +64,5 @@ NIconButton { PanelService.getPanel("archUpdaterPanel").toggle(screen, this) ArchUpdaterService.doPoll() - ArchUpdaterService.doAurPoll() } } diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 131f406..f155919 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -93,7 +93,7 @@ Loader { id: lockBgImage anchors.fill: parent fillMode: Image.PreserveAspectCrop - source: WallpaperService.getWallpaper(screen.name) + source: WallpaperService.currentWallpaper !== "" ? WallpaperService.currentWallpaper : "" cache: true smooth: true mipmap: false diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index a81a361..5dfcbfe 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -188,7 +188,7 @@ ColumnLayout { } NText { - text: `${Math.round(ScalingService.getMonitorScale(modelData.name) * 100)}%` + text: `${Math.round(ScalingService.scaleByName(modelData.name) * 100)}%` Layout.alignment: Qt.AlignVCenter Layout.minimumWidth: 50 * scaling horizontalAlignment: Text.AlignRight @@ -204,8 +204,12 @@ ColumnLayout { from: 0.7 to: 1.8 stepSize: 0.01 - value: ScalingService.getMonitorScale(modelData.name) - onPressedChanged: ScalingService.setMonitorScale(modelData.name, value) + value: ScalingService.scaleByName(modelData.name) + onPressedChanged: { + var data = Settings.data.monitorsScaling || {} + data[modelData.name] = value + Settings.data.monitorsScaling = data + } Layout.fillWidth: true Layout.minimumWidth: 150 * scaling } @@ -213,7 +217,11 @@ ColumnLayout { NIconButton { icon: "refresh" tooltipText: "Reset Scaling" - onClicked: ScalingService.setMonitorScale(modelData.name, 1.0) + onClicked: { + var data = Settings.data.monitorsScaling || {} + data[modelData.name] = 1.0 + Settings.data.monitorsScaling = data + } } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index b708f9d..0fa0338 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -23,12 +23,13 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: 140 * scaling radius: Style.radiusM * scaling - color: Color.mSecondary + color: Color.mPrimary NImageRounded { + id: currentWallpaperImage anchors.fill: parent anchors.margins: Style.marginXS * scaling - imagePath: WallpaperService.getWallpaper(screen.name) + imagePath: WallpaperService.currentWallpaper fallbackIcon: "image" imageRadius: Style.radiusM * scaling } @@ -61,44 +62,41 @@ ColumnLayout { wrapMode: Text.WordWrap Layout.fillWidth: true } + + NText { + text: Settings.data.wallpaper.swww.enabled ? "Wallpapers will change with " + Settings.data.wallpaper.swww.transitionType + + " transition." : "Wallpapers will change instantly." + color: Color.mOnSurface + font.pointSize: Style.fontSizeXS * scaling + visible: Settings.data.wallpaper.swww.enabled + } } NIconButton { icon: "refresh" tooltipText: "Refresh wallpaper list" onClicked: { - WallpaperService.refreshWallpapersList() + WallpaperService.listWallpapers() } - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.alignment: Qt.AlignTop | Qt.AlignRight } } - property list wallpapersList: WallpaperService.getWallpapersList(screen.name) - - NToggle { - label: "Assign selection to all monitors" - description: "Set selected wallpaper on all monitors at once." - checked: Settings.data.wallpaper.setWallpaperOnAllMonitors - onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked - visible: (wallpapersList.length > 0) - } - // Wallpaper grid container Item { - visible: !WallpaperService.scanning Layout.fillWidth: true Layout.preferredHeight: { - return Math.ceil(wallpapersList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight + return Math.ceil(WallpaperService.wallpaperList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight } GridView { id: wallpaperGridView anchors.fill: parent clip: true - model: wallpapersList + model: WallpaperService.wallpaperList boundsBehavior: Flickable.StopAtBounds - flickableDirection: Flickable.VerticalFlick + flickableDirection: Flickable.AutoFlickDirection interactive: false property int columns: 5 @@ -116,7 +114,7 @@ ColumnLayout { id: wallpaperItem property string wallpaperPath: modelData - property bool isSelected: wallpaperPath === WallpaperService.getWallpaper(screen.name) + property bool isSelected: wallpaperPath === WallpaperService.currentWallpaper width: wallpaperGridView.itemSize height: Math.floor(wallpaperGridView.itemSize * 0.67) @@ -181,65 +179,46 @@ ColumnLayout { acceptedButtons: Qt.LeftButton hoverEnabled: true onClicked: { - if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { - WallpaperService.changeWallpaper(undefined, wallpaperPath) - } else { - WallpaperService.changeWallpaper(screen.name, wallpaperPath) - } + WallpaperService.changeWallpaper(wallpaperPath) } } } } - } - // Empty state - Rectangle { - color: Color.mSurface - radius: Style.radiusM * scaling - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - visible: wallpapersList.length === 0 || WallpaperService.scanning - Layout.fillWidth: true - Layout.preferredHeight: 130 * scaling - - ColumnLayout { + // Empty state + Rectangle { anchors.fill: parent - visible: WallpaperService.scanning - NBusyIndicator { - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - } - } + color: Color.mSurface + radius: Style.radiusM * scaling + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + visible: WallpaperService.wallpaperList.length === 0 && !WallpaperService.scanning - ColumnLayout { - anchors.fill: parent - visible: wallpapersList.length === 0 && !WallpaperService.scanning - Item { - Layout.fillHeight: true - } + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM * scaling - NIcon { - text: "folder_open" - font.pointSize: Style.fontSizeXL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "folder_open" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No wallpaper found." - color: Color.mOnSurface - font.weight: Style.fontWeightBold - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No wallpapers found" + color: Color.mOnSurface + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "Make sure your wallpaper directory is configured and contains image files." - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.alignment: Qt.AlignHCenter - } - - Item { - Layout.fillHeight: true + NText { + text: "Make sure your wallpaper directory is configured and contains image files." + color: Color.mOnSurface + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: Style.sliderWidth * 1.5 * scaling + } } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 546a4ca..e96b5ef 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell import Quickshell.Io import qs.Commons import qs.Services @@ -10,62 +9,36 @@ import qs.Widgets ColumnLayout { id: root - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true - NTextInput { - label: "Wallpaper Directory" - description: "Path to your common wallpaper directory." - text: Settings.data.wallpaper.directory - onEditingFinished: { - Settings.data.wallpaper.directory = text - } - Layout.maximumWidth: 420 * scaling - } + // Process to check if swww is installed + Process { + id: swwwCheck + command: ["which", "swww"] + running: false - // Monitor-specific directories - NToggle { - label: "Monitor-specific directories" - description: "Enable multi-monitor wallpaper directory management." - checked: Settings.data.wallpaper.enableMultiMonitorDirectories - onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked - } - - NBox { - visible: Settings.data.wallpaper.enableMultiMonitorDirectories - - Layout.fillWidth: true - Layout.minimumWidth: 550 * scaling - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling - - ColumnLayout { - id: contentCol - anchors.fill: parent - anchors.margins: Style.marginXL * scaling - spacing: Style.marginM * scaling - Repeater { - model: Quickshell.screens || [] - delegate: RowLayout { - NText { - text: (modelData.name || "Unknown") - color: Color.mSecondary - font.weight: Style.fontWeightBold - Layout.preferredWidth: 90 * scaling - } - NTextInput { - Layout.fillWidth: true - text: WallpaperService.getMonitorDirectory(modelData.name) - onEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text) - Layout.maximumWidth: 420 * scaling - } - } - } + onExited: function (exitCode) { + if (exitCode === 0) { + // SWWW exists, enable it + Settings.data.wallpaper.swww.enabled = true + WallpaperService.startSWWWDaemon() + ToastService.showNotice("Swww", "Enabled") + } else { + // SWWW not found + ToastService.showWarning("Swww", "Not installed") } } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + NTextInput { + label: "Wallpaper Directory" + description: "Path to your wallpaper directory." + text: Settings.data.wallpaper.directory + onEditingFinished: { + Settings.data.wallpaper.directory = text + } + Layout.maximumWidth: 420 * scaling } NDivider { @@ -89,42 +62,10 @@ ColumnLayout { NToggle { label: "Random Wallpaper" description: "Automatically select random wallpapers from the folder." - checked: Settings.data.wallpaper.randomEnabled - onToggled: checked => Settings.data.wallpaper.randomEnabled = checked - } - - // Transition Type - NComboBox { - label: "Transition Type" - description: "Animation type when switching between wallpapers." - model: WallpaperService.transitionsModel - currentKey: Settings.data.wallpaper.transitionType - onSelected: key => Settings.data.wallpaper.transitionType = key - } - - // Transition Duration - ColumnLayout { - NLabel { - label: "Transition Duration" - description: "Duration of transition animations in seconds." - } - - RowLayout { - spacing: Style.marginL * scaling - NSlider { - Layout.fillWidth: true - from: 100 - to: 5000 - stepSize: 100 - value: Settings.data.wallpaper.transitionDuration - onMoved: Settings.data.wallpaper.transitionDuration = value - cutoutColor: Color.mSurface - } - NText { - text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(2) + "s" - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - } - } + checked: Settings.data.wallpaper.isRandom + onToggled: checked => { + Settings.data.wallpaper.isRandom = checked + } } // Interval (slider + H:M inputs) @@ -138,28 +79,25 @@ ColumnLayout { NText { // Show friendly H:MM format from current settings - text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomIntervalSec) + text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval) Layout.alignment: Qt.AlignBottom | Qt.AlignRight } } - // Preset chips using Repeater + // Preset chips RowLayout { id: presetRow spacing: Style.marginS * scaling - // Factorized presets data - property var intervalPresets: [5 * 60, 10 * 60, 15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60] - + // Preset seconds list + property var presets: [15 * 60, 30 * 60, 45 * 60, 60 * 60, 90 * 60, 120 * 60] // Whether current interval equals one of the presets - property bool isCurrentPreset: { - return intervalPresets.some(seconds => seconds === Settings.data.wallpaper.randomIntervalSec) - } + property bool isCurrentPreset: presets.indexOf(Settings.data.wallpaper.randomInterval) !== -1 // Allow user to force open the custom input; otherwise it's auto-open when not a preset property bool customForcedVisible: false function setIntervalSeconds(sec) { - Settings.data.wallpaper.randomIntervalSec = sec + Settings.data.wallpaper.randomInterval = sec WallpaperService.restartRandomWallpaperTimer() // Hide custom when selecting a preset customForcedVisible = false @@ -167,25 +105,168 @@ ColumnLayout { // Helper to color selected chip function isSelected(sec) { - return Settings.data.wallpaper.randomIntervalSec === sec + return Settings.data.wallpaper.randomInterval === sec } - // Repeater for preset chips - Repeater { - model: presetRow.intervalPresets - delegate: IntervalPresetChip { - seconds: modelData - label: Time.formatVagueHumanReadableDuration(modelData) - selected: presetRow.isSelected(modelData) - onClicked: presetRow.setIntervalSeconds(modelData) + // 15m + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(15 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label15.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(15 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(15 * 60) + } + NText { + id: label15 + anchors.centerIn: parent + text: "15m" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(15 * 60) ? Color.mOnPrimary : Color.mOnSurface + } + } + + // 30m + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(30 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label30.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(30 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(30 * 60) + } + NText { + id: label30 + anchors.centerIn: parent + text: "30m" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(30 * 60) ? Color.mOnPrimary : Color.mOnSurface + } + } + + // 45m + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(45 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label45.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(45 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(45 * 60) + } + NText { + id: label45 + anchors.centerIn: parent + text: "45m" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(45 * 60) ? Color.mOnPrimary : Color.mOnSurface + } + } + + // 1h + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(60 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label1h.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(60 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(60 * 60) + } + NText { + id: label1h + anchors.centerIn: parent + text: "1h" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(60 * 60) ? Color.mOnPrimary : Color.mOnSurface + } + } + + // 1h 30m + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(90 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label90.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(90 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(90 * 60) + } + NText { + id: label90 + anchors.centerIn: parent + text: "1h 30m" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(90 * 60) ? Color.mOnPrimary : Color.mOnSurface + } + } + + // 2h + Rectangle { + radius: height * 0.5 + color: presetRow.isSelected(120 * 60) ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: label2h.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: presetRow.isSelected(120 * 60) ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.setIntervalSeconds(120 * 60) + } + NText { + id: label2h + anchors.centerIn: parent + text: "2h" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: presetRow.isSelected(120 * 60) ? Color.mOnPrimary : Color.mOnSurface } } // Custom… opens inline input - IntervalPresetChip { - label: customRow.visible ? "Custom" : "Custom…" - selected: customRow.visible - onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible + Rectangle { + radius: height * 0.5 + color: customRow.visible ? Color.mPrimary : Color.mSurfaceVariant + implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) + implicitWidth: labelCustom.implicitWidth + Style.marginM * 1.5 * scaling + border.width: 1 + border.color: customRow.visible ? Color.transparent : Color.mOutline + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: presetRow.customForcedVisible = !presetRow.customForcedVisible + } + NText { + id: labelCustom + anchors.centerIn: parent + text: customRow.visible ? "Custom" : "Custom…" + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + color: customRow.visible ? Color.mOnPrimary : Color.mOnSurface + } } } @@ -201,11 +282,12 @@ ColumnLayout { description: "Enter time as HH:MM (e.g., 01:30)." inputMaxWidth: 100 * scaling text: { - const s = Settings.data.wallpaper.randomIntervalSec + const s = Settings.data.wallpaper.randomInterval const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) return h + ":" + (m < 10 ? ("0" + m) : m) } + onEditingFinished: { const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) if (m) { @@ -215,7 +297,7 @@ ColumnLayout { return h = Math.max(0, Math.min(24, h)) min = Math.max(0, Math.min(59, min)) - Settings.data.wallpaper.randomIntervalSec = (h * 3600) + (min * 60) + Settings.data.wallpaper.randomInterval = (h * 3600) + (min * 60) WallpaperService.restartRandomWallpaperTimer() // Keep custom visible after manual entry presetRow.customForcedVisible = true @@ -226,32 +308,193 @@ ColumnLayout { } } - // Reusable component for interval preset chips - component IntervalPresetChip: Rectangle { - property int seconds: 0 - property string label: "" - property bool selected: false - signal clicked + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } - radius: height * 0.5 - color: selected ? Color.mPrimary : Color.mSurfaceVariant - implicitHeight: Math.max(Style.baseWidgetSize * 0.55 * scaling, 24 * scaling) - implicitWidth: chipLabel.implicitWidth + Style.marginM * 1.5 * scaling - border.width: 1 - border.color: selected ? Color.transparent : Color.mOutline - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: parent.clicked() - } + // ------------------------------- + // Swww + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true NText { - id: chipLabel - anchors.centerIn: parent - text: parent.label - font.pointSize: Style.fontSizeS * scaling - color: parent.selected ? Color.mOnPrimary : Color.mOnSurface + text: "Swww" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + // Use SWWW + NToggle { + label: "Use Swww" + description: "Use Swww daemon for advanced wallpaper management." + checked: Settings.data.wallpaper.swww.enabled + onToggled: checked => { + if (checked) { + // Check if swww is installed + swwwCheck.running = true + } else { + Settings.data.wallpaper.swww.enabled = false + ToastService.showNotice("Swww", "Disabled") + } + } + } + + // SWWW Settings (only visible when useSWWW is enabled) + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginS * scaling + visible: Settings.data.wallpaper.swww.enabled + + // Resize Mode + NComboBox { + label: "Resize Mode" + description: "How Swww should resize wallpapers to fit the screen." + model: ListModel { + ListElement { + key: "no" + name: "No" + } + ListElement { + key: "crop" + name: "Crop" + } + ListElement { + key: "fit" + name: "Fit" + } + ListElement { + key: "stretch" + name: "Stretch" + } + } + currentKey: Settings.data.wallpaper.swww.resizeMethod + onSelected: key => { + Settings.data.wallpaper.swww.resizeMethod = key + } + } + + // Transition Type + NComboBox { + label: "Transition Type" + description: "Animation type when switching between wallpapers." + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "simple" + name: "Simple" + } + ListElement { + key: "fade" + name: "Fade" + } + ListElement { + key: "left" + name: "Left" + } + ListElement { + key: "right" + name: "Right" + } + ListElement { + key: "top" + name: "Top" + } + ListElement { + key: "bottom" + name: "Bottom" + } + ListElement { + key: "wipe" + name: "Wipe" + } + ListElement { + key: "wave" + name: "Wave" + } + ListElement { + key: "grow" + name: "Grow" + } + ListElement { + key: "center" + name: "Center" + } + ListElement { + key: "any" + name: "Any" + } + ListElement { + key: "outer" + name: "Outer" + } + ListElement { + key: "random" + name: "Random" + } + } + currentKey: Settings.data.wallpaper.swww.transitionType + onSelected: key => { + Settings.data.wallpaper.swww.transitionType = key + } + } + + // Transition FPS + ColumnLayout { + NLabel { + label: "Transition FPS" + description: "Frames per second for transition animations." + } + + RowLayout { + spacing: Style.marginL * scaling + NSlider { + Layout.fillWidth: true + from: 30 + to: 500 + stepSize: 5 + value: Settings.data.wallpaper.swww.transitionFps + onMoved: Settings.data.wallpaper.swww.transitionFps = Math.round(value) + cutoutColor: Color.mSurface + } + NText { + text: Settings.data.wallpaper.swww.transitionFps + " FPS" + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + } + + // Transition Duration + ColumnLayout { + NLabel { + label: "Transition Duration" + description: "Duration of transition animations in seconds." + } + + RowLayout { + spacing: Style.marginL * scaling + NSlider { + Layout.fillWidth: true + from: 0.25 + to: 10 + stepSize: 0.05 + value: Settings.data.wallpaper.swww.transitionDuration + onMoved: Settings.data.wallpaper.swww.transitionDuration = value + cutoutColor: Color.mSurface + } + NText { + text: Settings.data.wallpaper.swww.transitionDuration.toFixed(2) + "s" + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + } + } + } } } diff --git a/README.md b/README.md index b701e0e..9429f2d 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c ## Preview -![Launcher](/Assets/Screenshots/launcher.png) +![Launcher](https://assets.noctalia.dev/screenshots/launcher.png?v=2) -![SettingsPanel](/Assets/Screenshots/settings-panel.png?v=2) +![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png?v=2) -![SidePanel](/Assets/Screenshots/light-mode.png?v=2) +![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png?v=2) --- @@ -75,14 +75,21 @@ Features a modern modular architecture with a status bar, notification system, c ### Optional - `cliphist` - For clipboard history support +- `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component - `wlsunset` - To be able to use NightLight -> There are 2 more optional dependencies. -> Any `polkit agent` to be able to use the ArchUpdater widget. +> There is one more optional dependency. > And also any `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder. +If you want to use the ArchUpdater please make sure to set your terminal with the `TERMINAL` environment variable. +``` +sudo sed -i '/^TERMINAL=/d' /etc/environment && echo 'TERMINAL=/usr/bin/kitty' | sudo tee -a /etc/environment + +``` +In that command you **NEED** to replace kitty with whatever your terminal is (and perhaps edit the path depending on the distro). + --- ## Quick Start @@ -269,6 +276,14 @@ The launcher supports special commands for enhanced functionality: ## Advanced Configuration +### Niri Configuration + +Add this to your `layout` section for proper swww integration: + +``` +background-color "transparent" +``` + ### Recommended Compositor Settings For Niri: @@ -279,6 +294,11 @@ window-rule { clip-to-geometry true } +layer-rule { + match namespace="^swww-daemon$" + place-within-backdrop true +} + layer-rule { match namespace="^quickshell-wallpaper$" } diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index de3b3af..cd40a1d 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -8,28 +8,164 @@ import qs.Commons Singleton { id: updateService - // Core properties - readonly property bool busy: checkupdatesProcess.running - readonly property bool aurBusy: checkAurUpdatesProcess.running - readonly property int updates: repoPackages.length - readonly property int aurUpdates: aurPackages.length - readonly property int totalUpdates: updates + aurUpdates + // ============================================================================ + // CORE PROPERTIES + // ============================================================================ + + // Package data property var repoPackages: [] property var aurPackages: [] property var selectedPackages: [] property int selectedPackagesCount: 0 + + // Update state property bool updateInProgress: false + property bool updateFailed: false + property string lastUpdateError: "" + + // Computed properties + readonly property bool busy: checkupdatesProcess.running + readonly property bool aurBusy: checkParuUpdatesProcess.running + readonly property int updates: repoPackages.length + readonly property int aurUpdates: aurPackages.length + readonly property int totalUpdates: updates + aurUpdates + + // ============================================================================ + // TIMERS + // ============================================================================ + + // Refresh timer for post-update polling + Timer { + id: refreshTimer + interval: 5000 // Increased delay to ensure updates complete + repeat: false + onTriggered: { + console.log("ArchUpdater: Refreshing package lists after update...") + // Just refresh package lists without syncing database + doPoll() + } + } + + // Timer to mark update as complete - with error handling + Timer { + id: updateCompleteTimer + interval: 30000 // Increased to 30 seconds to allow more time + repeat: false + onTriggered: { + console.log("ArchUpdater: Update timeout reached, checking for failures...") + checkForUpdateFailures() + } + } + + // Timer to check if update processes are still running + Timer { + id: updateMonitorTimer + interval: 2000 + repeat: true + running: updateInProgress + onTriggered: { + // Check if any update-related processes might still be running + checkUpdateStatus() + } + } + + // ============================================================================ + // MONITORING PROCESSES + // ============================================================================ + + // Process to monitor update completion + Process { + id: updateStatusProcess + command: ["pgrep", "-f", "(pacman|yay|paru).*(-S|-Syu)"] + onExited: function (exitCode) { + if (exitCode !== 0 && updateInProgress) { + // No update processes found, update likely completed + console.log("ArchUpdater: No update processes detected, marking update as complete") + updateInProgress = false + updateMonitorTimer.stop() + + // Don't stop the complete timer - let it handle failures + // If the update actually failed, the timer will trigger and set updateFailed = true + + // Refresh package lists after a short delay + Qt.callLater(() => { + doPoll() + }, 2000) + } + } + } + + // Process to check for errors in log file (only when update is in progress) + Process { + id: errorCheckProcess + command: ["sh", "-c", "if [ -f /tmp/archupdater_output.log ]; then grep -i 'error\\|failed\\|failed to build\\|ERROR_DETECTED' /tmp/archupdater_output.log | tail -1; fi"] + onExited: function (exitCode) { + if (exitCode === 0 && updateInProgress) { + // Error found in log + console.log("ArchUpdater: Error detected in log file") + updateInProgress = false + updateFailed = true + updateCompleteTimer.stop() + updateMonitorTimer.stop() + lastUpdateError = "Build or update error detected" + + // Refresh to check actual state + Qt.callLater(() => { + doPoll() + }, 1000) + } + } + } + + // Timer to check for errors more frequently when update is in progress + Timer { + id: errorCheckTimer + interval: 5000 // Check every 5 seconds + repeat: true + running: updateInProgress + onTriggered: { + if (updateInProgress && !errorCheckProcess.running) { + errorCheckProcess.running = true + } + } + } + + // ============================================================================ + // MONITORING FUNCTIONS + // ============================================================================ + function checkUpdateStatus() { + if (updateInProgress && !updateStatusProcess.running) { + updateStatusProcess.running = true + } + } + + function checkForUpdateFailures() { + console.log("ArchUpdater: Checking for update failures...") + updateInProgress = false + updateFailed = true + updateCompleteTimer.stop() + updateMonitorTimer.stop() + + // Refresh to check actual state after a delay + Qt.callLater(() => { + doPoll() + }, 2000) + } // Initial check Component.onCompleted: { + getAurHelper() doPoll() - doAurPoll() } + // ============================================================================ + // PACKAGE CHECKING PROCESSES + // ============================================================================ + // Process for checking repo updates Process { id: checkupdatesProcess - command: ["checkupdates"] + command: ["checkupdates", "--nosync"] onExited: function (exitCode) { if (exitCode !== 0 && exitCode !== 2) { Logger.warn("ArchUpdater", "checkupdates failed (code:", exitCode, ")") @@ -44,13 +180,13 @@ Singleton { } } - // Process for checking AUR updates + // Process for checking AUR updates with paru specifically Process { - id: checkAurUpdatesProcess - command: ["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Qua || command -v paru >/dev/null 2>&1 && paru -Qua || echo ''"] + id: checkParuUpdatesProcess + command: ["paru", "-Qua"] onExited: function (exitCode) { if (exitCode !== 0) { - Logger.warn("ArchUpdater", "AUR check failed (code:", exitCode, ")") + Logger.warn("ArchUpdater", "paru check failed (code:", exitCode, ")") aurPackages = [] } } @@ -62,8 +198,12 @@ Singleton { } } - // Parse checkupdates output - function parseCheckupdatesOutput(output) { + // ============================================================================ + // PARSING FUNCTIONS + // ============================================================================ + + // Generic package parsing function + function parsePackageOutput(output, source) { const lines = output.trim().split('\n').filter(line => line.trim()) const packages = [] @@ -75,65 +215,83 @@ Singleton { "oldVersion": m[2], "newVersion": m[3], "description": `${m[1]} ${m[2]} -> ${m[3]}`, - "source": "repo" + "source": source }) } } - repoPackages = packages + // Only update if we have new data or if this is a fresh check + if (packages.length > 0 || output.trim() === "") { + if (source === "repo") { + repoPackages = packages + } else { + aurPackages = packages + } + } + } + + // Parse checkupdates output + function parseCheckupdatesOutput(output) { + parsePackageOutput(output, "repo") } // Parse AUR updates output function parseAurUpdatesOutput(output) { - const lines = output.trim().split('\n').filter(line => line.trim()) - const packages = [] + parsePackageOutput(output, "aur") + } - 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]}`, - "source": "aur" - }) - } + function doPoll() { + // Start repo updates check + if (!busy) { + checkupdatesProcess.running = true } - aurPackages = packages + // Start AUR updates check + if (!aurBusy) { + checkParuUpdatesProcess.running = true + } } - // Check for updates - function doPoll() { - if (busy) - return - checkupdatesProcess.running = true - } + // ============================================================================ + // UPDATE FUNCTIONS + // ============================================================================ - // Check for AUR updates - function doAurPoll() { - if (aurBusy) - return - checkAurUpdatesProcess.running = true + // Helper function to generate update command with error detection + function generateUpdateCommand(baseCommand) { + return baseCommand + " 2>&1 | tee /tmp/archupdater_output.log; if [ $? -ne 0 ]; then echo 'ERROR_DETECTED'; fi; echo 'Update complete! Press Enter to close...'; read -p 'Press Enter to continue...'" } // Update all packages (repo + AUR) function runUpdate() { if (totalUpdates === 0) { doPoll() - doAurPoll() return } + // Reset any previous error states + updateFailed = false + lastUpdateError = "" updateInProgress = true - // Update repos first, then AUR - Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"]) - Quickshell.execDetached( - ["sh", "-c", "command -v yay >/dev/null 2>&1 && yay -Sua --noconfirm || command -v paru >/dev/null 2>&1 && paru -Sua --noconfirm || true"]) + console.log("ArchUpdater: Starting full system update...") - // Refresh after updates with multiple attempts - refreshAfterUpdate() + const terminal = Quickshell.env("TERMINAL") || "xterm" + + // Check if we have an AUR helper for full system update + const aurHelper = getAurHelper() + if (aurHelper && (aurUpdates > 0 || updates > 0)) { + // Use AUR helper for full system update (handles both repo and AUR) + const command = generateUpdateCommand(aurHelper + " -Syu") + Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) + } else if (updates > 0) { + // Fallback to pacman if no AUR helper or only repo updates + const command = generateUpdateCommand("sudo pacman -Syu") + Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) + } + + // Start monitoring and timeout timers + refreshTimer.start() + updateCompleteTimer.start() + updateMonitorTimer.start() } // Update selected packages @@ -141,7 +299,13 @@ Singleton { if (selectedPackages.length === 0) return + // Reset any previous error states + updateFailed = false + lastUpdateError = "" updateInProgress = true + console.log("ArchUpdater: Starting selective update for", selectedPackages.length, "packages") + + const terminal = Quickshell.env("TERMINAL") || "xterm" // Split selected packages by source const repoPkgs = [] @@ -159,48 +323,140 @@ Singleton { } } - // Update repo packages + // Update repo packages with sudo if (repoPkgs.length > 0) { - const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs) - Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" ")) - Quickshell.execDetached(repoCommand) + const packageList = repoPkgs.join(" ") + const command = generateUpdateCommand("sudo pacman -S " + packageList) + Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) } - // Update AUR packages + // Update AUR packages with yay/paru if (aurPkgs.length > 0) { const aurHelper = getAurHelper() if (aurHelper) { - const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs) - Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" ")) - Quickshell.execDetached(aurCommand) + const packageList = aurPkgs.join(" ") + const command = generateUpdateCommand(aurHelper + " -S " + packageList) + Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) } else { Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", ")) } } - // Clear selection and refresh - selectedPackages = [] - selectedPackagesCount = 0 - refreshAfterUpdate() + // Start monitoring and timeout timers + refreshTimer.start() + updateCompleteTimer.start() + updateMonitorTimer.start() } + // Reset update state (useful for manual recovery) + function resetUpdateState() { + // If update is in progress, mark it as failed first + if (updateInProgress) { + updateFailed = true + } + + updateInProgress = false + lastUpdateError = "" + updateCompleteTimer.stop() + updateMonitorTimer.stop() + refreshTimer.stop() + + // Refresh to get current state + doPoll() + } + + // Manual refresh function + function forceRefresh() { + // Prevent multiple simultaneous refreshes + if (busy || aurBusy) { + return + } + + // Clear error states when refreshing + updateFailed = false + lastUpdateError = "" + + // Just refresh the package lists without syncing databases + doPoll() + } + + // ============================================================================ + // UTILITY PROCESSES + // ============================================================================ + + // Process for checking yay availability + Process { + id: yayCheckProcess + command: ["which", "yay"] + onExited: function (exitCode) { + if (exitCode === 0) { + cachedAurHelper = "yay" + } + } + } + + // Process for checking paru availability + Process { + id: paruCheckProcess + command: ["which", "paru"] + onExited: function (exitCode) { + if (exitCode === 0) { + if (cachedAurHelper === "") { + cachedAurHelper = "paru" + } + } + } + } + + // Process for syncing package databases with sudo + Process { + id: syncDatabaseProcess + command: ["sudo", "pacman", "-Sy"] + onStarted: { + console.log("ArchUpdater: Starting database sync with sudo...") + } + onExited: function (exitCode) { + console.log("ArchUpdater: Database sync exited with code:", exitCode) + if (exitCode === 0) { + console.log("ArchUpdater: Database sync successful") + } else { + console.log("ArchUpdater: Database sync failed") + } + + // After sync completes, wait a moment then refresh package lists + console.log("ArchUpdater: Database sync complete, waiting before refresh...") + Qt.callLater(() => { + console.log("ArchUpdater: Refreshing package lists after database sync...") + doPoll() + }, 2000) + } + } + + // Cached AUR helper detection + property string cachedAurHelper: "" + // Helper function to detect AUR helper function getAurHelper() { - // Check for yay first, then paru - const yayCheck = Quickshell.exec("command -v yay", true) - if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) { - return "yay" + // Return cached result if available + if (cachedAurHelper !== "") { + return cachedAurHelper } - const paruCheck = Quickshell.exec("command -v paru", true) - if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) { - return "paru" - } + // Check for AUR helpers using Process objects + console.log("ArchUpdater: Detecting AUR helper...") - return null + // Start the detection processes + yayCheckProcess.running = true + paruCheckProcess.running = true + + // For now, return a default (will be updated by the processes) + // In a real implementation, you'd want to wait for the processes to complete + return "paru" // Default fallback } - // Package selection functions + // ============================================================================ + // PACKAGE SELECTION FUNCTIONS + // ============================================================================ function togglePackageSelection(packageName) { const index = selectedPackages.indexOf(packageName) if (index > -1) { @@ -225,47 +481,60 @@ Singleton { return selectedPackages.indexOf(packageName) > -1 } - // Robust refresh after updates - function refreshAfterUpdate() { - // First refresh attempt after 3 seconds - Qt.callLater(() => { - doPoll() - doAurPoll() - }, 3000) + // ============================================================================ + // REFRESH FUNCTIONS + // ============================================================================ - // Second refresh attempt after 8 seconds - Qt.callLater(() => { - doPoll() - doAurPoll() - }, 8000) - - // Third refresh attempt after 15 seconds - Qt.callLater(() => { - doPoll() - doAurPoll() - updateInProgress = false - }, 15000) - - // Final refresh attempt after 30 seconds - Qt.callLater(() => { - doPoll() - doAurPoll() - }, 30000) + // Function to manually sync package databases (separate from refresh) + function syncPackageDatabases() { + console.log("ArchUpdater: Manual database sync requested...") + const terminal = Quickshell.env("TERMINAL") || "xterm" + const command = "sudo pacman -Sy && echo 'Database sync complete! Press Enter to close...' && read -p 'Press Enter to continue...'" + console.log("ArchUpdater: Executing sync command:", command) + console.log("ArchUpdater: Terminal:", terminal) + Quickshell.execDetached([terminal, "-e", "bash", "-c", command]) } + // Function to force a complete refresh (sync + check) + function forceCompleteRefresh() { + console.log("ArchUpdater: Force complete refresh requested...") + + // Start database sync process (will trigger refresh when complete) + console.log("ArchUpdater: Starting complete refresh process...") + syncDatabaseProcess.running = true + } + + // Function to sync database and refresh package lists + function syncDatabaseAndRefresh() { + console.log("ArchUpdater: Syncing database and refreshing package lists...") + + // Start database sync process (will trigger refresh when complete) + console.log("ArchUpdater: Starting database sync process...") + syncDatabaseProcess.running = true + } + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + // Notification helper function notify(title, body) { Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body]) } + // ============================================================================ + // AUTO-POLL TIMER + // ============================================================================ + // Auto-poll every 15 minutes Timer { interval: 15 * 60 * 1000 // 15 minutes repeat: true running: true onTriggered: { - doPoll() - doAurPoll() + if (!updateInProgress) { + doPoll() + } } } } diff --git a/Services/MatugenService.qml b/Services/MatugenService.qml index 9c40aaf..1749410 100644 --- a/Services/MatugenService.qml +++ b/Services/MatugenService.qml @@ -22,11 +22,9 @@ Singleton { // Ensure cache dir exists Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) - Logger.log("Matugen", "Generating from wallpaper on screen:", Screen.name) - var wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''") - var content = buildConfigToml() var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light" + var wp = WallpaperService.currentWallpaper.replace(/'/g, "'\\''") var pathEsc = dynamicConfigPath.replace(/'/g, "'\\''") var extraRepo = (Quickshell.shellDir + "/Assets/Matugen/extra").replace(/'/g, "'\\''") var extraUser = (Settings.configDir + "matugen.d").replace(/'/g, "'\\''") diff --git a/Services/ScalingService.qml b/Services/ScalingService.qml index 8aa32bb..2b6cc58 100644 --- a/Services/ScalingService.qml +++ b/Services/ScalingService.qml @@ -11,7 +11,7 @@ Singleton { function scale(aScreen) { try { if (aScreen !== undefined && aScreen.name !== undefined) { - return getMonitorScale(aScreen.name) + return scaleByName(aScreen.name) } } catch (e) { @@ -20,46 +20,21 @@ Singleton { return 1.0 } - // ------------------------------------------- - function getMonitorScale(aScreenName) { + function scaleByName(aScreenName) { try { - var monitors = Settings.data.ui.monitorsScaling - if (monitors !== undefined) { - for (var i = 0; i < monitors.length; i++) { - if (monitors[i].name !== undefined && monitors[i].name === aScreenName) { - return monitors[i].scale - } + if (Settings.data.monitorsScaling !== undefined) { + if (Settings.data.monitorsScaling[aScreenName] !== undefined) { + return Settings.data.monitorsScaling[aScreenName] } } } catch (e) { //Logger.warn(e) } + return 1.0 } - // ------------------------------------------- - function setMonitorScale(aScreenName, scale) { - try { - var monitors = Settings.data.ui.monitorsScaling - if (monitors !== undefined) { - for (var i = 0; i < monitors.length; i++) { - if (monitors[i].name !== undefined && monitors[i].name === aScreenName) { - monitors[i].scale = scale - return - } - } - } - monitors.push({ - "name": aScreenName, - "scale": scale - }) - } catch (e) { - - //Logger.warn(e) - } - } - // ------------------------------------------- // Dynamic scaling based on resolution diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index 15742c3..0b43142 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -10,197 +10,84 @@ Singleton { id: root Component.onCompleted: { - Logger.log("Wallpaper", "Service started") + Logger.log("Wallpapers", "Service started") + listWallpapers() + + // Wallpaper is set when the settings are loaded. + // Don't start random wallpaper during initialization } - // All available wallpaper transitions - readonly property ListModel transitionsModel: ListModel { - ListElement { - key: "none" - name: "None" - } - ListElement { - key: "fade" - name: "Fade" - } - ListElement { - key: "wipe_left" - name: "Wipe Left" - } - ListElement { - key: "wipe_right" - name: "Wipe Right" - } - ListElement { - key: "wipe_up" - name: "Wipe Up" - } - ListElement { - key: "wipe_down" - name: "Wipe Down" - } + property var wallpaperList: [] + property string currentWallpaper: Settings.data.wallpaper.current + property bool scanning: false + + // SWWW + property string transitionType: Settings.data.wallpaper.swww.transitionType + property var randomChoices: ["simple", "fade", "left", "right", "top", "bottom", "wipe", "wave", "grow", "center", "any", "outer"] + + function listWallpapers() { + Logger.log("Wallpapers", "Listing wallpapers") + scanning = true + wallpaperList = [] + // Set the folder directly to avoid model reset issues + folderModel.folder = "file://" + (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") } - property var wallpaperLists: ({}) - property int scanningCount: 0 - readonly property bool scanning: (scanningCount > 0) - - Connections { - target: Settings.data.wallpaper - function onDirectoryChanged() { - root.refreshWallpapersList() - } - function onRandomEnabledChanged() { - root.toggleRandomWallpaper() - } - function onRandomIntervalSecChanged() { - root.restartRandomWallpaperTimer() - } + function changeWallpaper(path) { + Logger.log("Wallpapers", "Changing to:", path) + setCurrentWallpaper(path, false) } - // ------------------------------------------------------------------- - // Get specific monitor wallpaper data - function getMonitorConfig(screenName) { - var monitors = Settings.data.wallpaper.monitors - if (monitors !== undefined) { - for (var i = 0; i < monitors.length; i++) { - if (monitors[i].name !== undefined && monitors[i].name === screenName) { - return monitors[i] - } + function setCurrentWallpaper(path, isInitial) { + // Only regenerate colors if the wallpaper actually changed + var wallpaperChanged = currentWallpaper !== path + + currentWallpaper = path + if (!isInitial) { + Settings.data.wallpaper.current = path + } + if (Settings.data.wallpaper.swww.enabled) { + if (Settings.data.wallpaper.swww.transitionType === "random") { + transitionType = randomChoices[Math.floor(Math.random() * randomChoices.length)] + } else { + transitionType = Settings.data.wallpaper.swww.transitionType } - } - } - // ------------------------------------------------------------------- - // Get specific monitor directory - function getMonitorDirectory(screenName) { - if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { - return Settings.data.wallpaper.directory - } - - var monitor = getMonitorConfig(screenName) - if (monitor !== undefined && monitor.directory !== undefined) { - return monitor.directory - } - - // Fall back to the main/single directory - return Settings.data.wallpaper.directory - } - - // ------------------------------------------------------------------- - // Set specific monitor directory - function setMonitorDirectory(screenName, directory) { - var monitor = getMonitorConfig(screenName) - if (monitor !== undefined) { - monitor.directory = directory + changeWallpaperProcess.running = true } else { - Settings.data.wallpaper.monitors.push({ - "name": screenName, - "directory": directory, - "wallpaper": "" - }) - } - } - // ------------------------------------------------------------------- - // Get specific monitor wallpaper - function getWallpaper(screenName) { - var monitor = getMonitorConfig(screenName) - if ((monitor !== undefined) && (monitor["wallpaper"] !== undefined)) { - return monitor["wallpaper"] - } - return "" - } - - // ------------------------------------------------------------------- - function changeWallpaper(screenName, path) { - if (screenName !== undefined) { - setWallpaper(screenName, path) - } else { - // If no screenName specified change for all screens - for (var i = 0; i < Quickshell.screens.length; i++) { - setWallpaper(Quickshell.screens[i].name, path) - } - } - } - - // ------------------------------------------------------------------- - function setWallpaper(screenName, path) { - if (path === "" || path === undefined) { - return + // Fallback: update the settings directly for non-SWWW mode + //Logger.log("Wallpapers", "Not using Swww, setting wallpaper directly") } - if (screenName === undefined) { - Logger.warn("Wallpaper", "setWallpaper", "no screen specified") - return - } - - Logger.log("Wallpaper", "setWallpaper on", screenName, ": ", path) - - var wallpaperChanged = false - - var monitor = getMonitorConfig(screenName) - if (monitor !== undefined) { - wallpaperChanged = (monitor["wallpaper"] !== path) - monitor["wallpaper"] = path - } else { - wallpaperChanged = true - Settings.data.wallpaper.monitors.push({ - "name": screenName, - "directory": getMonitorDirectory(screenName), - "wallpaper": path - }) - } - - // Restart the random wallpaper timer if (randomWallpaperTimer.running) { randomWallpaperTimer.restart() } - // Notify ColorScheme service if the wallpaper actually changed + // Only notify ColorScheme service if the wallpaper actually changed if (wallpaperChanged) { ColorSchemeService.changedWallpaper() } } - // ------------------------------------------------------------------- function setRandomWallpaper() { - Logger.log("Wallpaper", "setRandomWallpaper") - - if (Settings.data.wallpaper.enableMultiMonitorDirectories) { - // Pick a random wallpaper per screen - for (var i = 0; i < Quickshell.screens.length; i++) { - var screenName = Quickshell.screens[i].name - var wallpaperList = getWallpapersList(screenName) - - if (wallpaperList.length > 0) { - var randomIndex = Math.floor(Math.random() * wallpaperList.length) - var randomPath = wallpaperList[randomIndex] - changeWallpaper(screenName, randomPath) - } - } - } else { - // Pick a random wallpaper common to all screens - // We can use any screenName here, so we just pick the primary one. - var wallpaperList = getWallpapersList(Screen.name) - if (wallpaperList.length > 0) { - var randomIndex = Math.floor(Math.random() * wallpaperList.length) - var randomPath = wallpaperList[randomIndex] - changeWallpaper(undefined, randomPath) - } + var randomIndex = Math.floor(Math.random() * wallpaperList.length) + var randomPath = wallpaperList[randomIndex] + if (!randomPath) { + return } + setCurrentWallpaper(randomPath, false) } - // ------------------------------------------------------------------- function toggleRandomWallpaper() { - Logger.log("Wallpaper", "toggleRandomWallpaper") - if (Settings.data.wallpaper.randomEnabled) { - randomWallpaperTimer.restart() + if (Settings.data.wallpaper.isRandom && !randomWallpaperTimer.running) { + randomWallpaperTimer.start() setRandomWallpaper() + } else if (!Settings.data.randomWallpaper && randomWallpaperTimer.running) { + randomWallpaperTimer.stop() } } - // ------------------------------------------------------------------- function restartRandomWallpaperTimer() { if (Settings.data.wallpaper.isRandom) { randomWallpaperTimer.stop() @@ -208,81 +95,78 @@ Singleton { } } - // ------------------------------------------------------------------- - function getWallpapersList(screenName) { - if (screenName != undefined && wallpaperLists[screenName] != undefined) { - return wallpaperLists[screenName] - } - return [] - } - - // ------------------------------------------------------------------- - function refreshWallpapersList() { - Logger.log("Wallpaper", "refreshWallpapersList") - scanningCount = 0 - - // Force refresh by toggling the folder property on each FolderListModel - for (var i = 0; i < wallpaperScanners.count; i++) { - var scanner = wallpaperScanners.objectAt(i) - if (scanner) { - var currentFolder = scanner.folder - scanner.folder = "" - scanner.folder = currentFolder - } + function startSWWWDaemon() { + if (Settings.data.wallpaper.swww.enabled) { + Logger.log("Swww", "Requesting swww-daemon") + startDaemonProcess.running = true } } - // ------------------------------------------------------------------- - // ------------------------------------------------------------------- - // ------------------------------------------------------------------- Timer { id: randomWallpaperTimer - interval: Settings.data.wallpaper.randomIntervalSec * 1000 - running: Settings.data.wallpaper.randomEnabled + interval: Settings.data.wallpaper.randomInterval * 1000 + running: false repeat: true onTriggered: setRandomWallpaper() triggeredOnStart: false } - // Instantiator (not Repeater) to create FolderListModel for each monitor - Instantiator { - id: wallpaperScanners - model: Quickshell.screens - delegate: FolderListModel { - property string screenName: modelData.name - - folder: "file://" + root.getMonitorDirectory(screenName) - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] - showDirs: false - sortField: FolderListModel.Name - onStatusChanged: { - if (status === FolderListModel.Null) { - // Flush the list - var lists = root.wallpaperLists - lists[screenName] = [] - root.wallpaperLists = lists - } else if (status === FolderListModel.Loading) { - // Flush the list - var lists = root.wallpaperLists - lists[screenName] = [] - root.wallpaperLists = lists - - scanningCount++ - } else if (status === FolderListModel.Ready) { - var files = [] - for (var i = 0; i < count; i++) { - var directory = root.getMonitorDirectory(screenName) - var filepath = directory + "/" + get(i, "fileName") - files.push(filepath) - } - - var lists = root.wallpaperLists - lists[screenName] = files - root.wallpaperLists = lists - - scanningCount-- - Logger.log("Wallpaper", "List refreshed for", screenName, "count:", files.length) + FolderListModel { + id: folderModel + // Swww supports many images format but Quickshell only support a subset of those. + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"] + showDirs: false + sortField: FolderListModel.Name + onStatusChanged: { + if (status === FolderListModel.Ready) { + var files = [] + for (var i = 0; i < count; i++) { + var directory = (Settings.data.wallpaper.directory !== undefined ? Settings.data.wallpaper.directory : "") + var filepath = directory + "/" + get(i, "fileName") + files.push(filepath) } + wallpaperList = files + scanning = false + Logger.log("Wallpapers", "List refreshed, count:", wallpaperList.length) + } + } + } + + Process { + id: changeWallpaperProcess + command: ["swww", "img", "--resize", Settings.data.wallpaper.swww.resizeMethod, "--transition-fps", Settings.data.wallpaper.swww.transitionFps.toString( + ), "--transition-type", transitionType, "--transition-duration", Settings.data.wallpaper.swww.transitionDuration.toString( + ), currentWallpaper] + running: false + + onStarted: { + + } + + onExited: function (exitCode, exitStatus) { + Logger.log("Swww", "Process finished with exit code:", exitCode, "status:", exitStatus) + if (exitCode !== 0) { + Logger.log("Swww", "Process failed. Make sure swww-daemon is running with: swww-daemon") + Logger.log("Swww", "You can start it with: swww-daemon --format xrgb") + } + } + } + + Process { + id: startDaemonProcess + command: ["swww-daemon", "--format", "xrgb"] + running: false + + onStarted: { + Logger.log("Swww", "Daemon start process initiated") + } + + onExited: function (exitCode, exitStatus) { + Logger.log("Swww", "Daemon start process finished with exit code:", exitCode) + if (exitCode === 0) { + Logger.log("Swww", "Daemon started successfully") + } else { + Logger.log("Swww", "Failed to start daemon, may already be running") } } } diff --git a/Widgets/NLabel.qml b/Widgets/NLabel.qml index e05604c..cf05b8f 100644 --- a/Widgets/NLabel.qml +++ b/Widgets/NLabel.qml @@ -3,12 +3,8 @@ import QtQuick.Layouts import qs.Commons ColumnLayout { - id: root - property string label: "" property string description: "" - property color labelColor: Color.mOnSurface - property color descriptionColor: Color.mOnSurfaceVariant spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -17,14 +13,14 @@ ColumnLayout { text: label font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold - color: labelColor + color: Color.mOnSurface visible: label !== "" } NText { text: description font.pointSize: Style.fontSizeS * scaling - color: descriptionColor + color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap visible: description !== "" Layout.fillWidth: true diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index cad5077..6533af9 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -12,8 +12,6 @@ ColumnLayout { property bool readOnly: false property bool enabled: true property int inputMaxWidth: 420 * scaling - property color labelColor: Color.mOnSurface - property color descriptionColor: Color.mOnSurfaceVariant property alias text: input.text property alias placeholderText: input.placeholderText @@ -27,8 +25,6 @@ ColumnLayout { NLabel { label: root.label description: root.description - labelColor: root.labelColor - descriptionColor: root.descriptionColor visible: root.label !== "" || root.description !== "" } diff --git a/Widgets/NWidgetLoader.qml b/Widgets/NWidgetLoader.qml index c0a623e..a9e7ac5 100644 --- a/Widgets/NWidgetLoader.qml +++ b/Widgets/NWidgetLoader.qml @@ -35,7 +35,7 @@ Item { } } } - //Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name) + Logger.log("NWidgetLoader", "Loaded", widgetName, "on screen", item.screen.name) } } diff --git a/flake.nix b/flake.nix index b61342f..67aec51 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,7 @@ libnotify matugen networkmanager + swww wl-clipboard ];