diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index f9dfd5427..654ce3575 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -218,6 +218,7 @@ Singleton { property bool wallpaperCyclingEnabled: false property bool wallpaperCyclingRandom: false + property bool wallpaperCyclingRecursive: false property string wallpaperCyclingMode: "interval" property int wallpaperCyclingInterval: 300 property string wallpaperCyclingTime: "06:00" @@ -1243,6 +1244,11 @@ Singleton { saveSettings(); } + function setWallpaperCyclingRecursive(recursive) { + wallpaperCyclingRecursive = recursive; + saveSettings(); + } + function setWallpaperCyclingMode(mode) { wallpaperCyclingMode = mode; saveSettings(); @@ -1287,6 +1293,10 @@ Singleton { updateMonitorCyclingSetting(screenName, "random", random); } + function setMonitorCyclingRecursive(screenName, recursive) { + updateMonitorCyclingSetting(screenName, "recursive", recursive); + } + function setMonitorCyclingMode(screenName, mode) { updateMonitorCyclingSetting(screenName, "mode", mode); } @@ -1852,6 +1862,7 @@ Singleton { var defaults = { "enabled": false, "random": false, + "recursive": false, "mode": "interval", "interval": 300, "time": "06:00", diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index 6b42b345b..13f753a3b 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -60,6 +60,9 @@ var LOCAL_SPEC = { wallpaperCyclingRandom: { def: false }, + wallpaperCyclingRecursive: { + def: false + }, wallpaperCyclingMode: { def: "interval" }, diff --git a/quickshell/Modules/Settings/WallpaperCyclingTab.qml b/quickshell/Modules/Settings/WallpaperCyclingTab.qml index 6742c8ef0..e3e8b25dc 100644 --- a/quickshell/Modules/Settings/WallpaperCyclingTab.qml +++ b/quickshell/Modules/Settings/WallpaperCyclingTab.qml @@ -21,6 +21,7 @@ Item { } readonly property bool enabled: perMonitor ? monitorSettings.enabled : SessionData.wallpaperCyclingEnabled readonly property bool random: perMonitor ? monitorSettings.random : SessionData.wallpaperCyclingRandom + readonly property bool recursive: perMonitor ? monitorSettings.recursive : SessionData.wallpaperCyclingRecursive readonly property string mode: perMonitor ? monitorSettings.mode : SessionData.wallpaperCyclingMode readonly property int interval: perMonitor ? monitorSettings.interval : SessionData.wallpaperCyclingInterval readonly property string time: perMonitor ? monitorSettings.time : SessionData.wallpaperCyclingTime @@ -84,6 +85,14 @@ Item { SessionData.setWallpaperCyclingRandom(value); } + function setRecursive(value) { + if (perMonitor) { + SessionData.setMonitorCyclingRecursive(selectedScreen, value); + return; + } + SessionData.setWallpaperCyclingRecursive(value); + } + function setMode(value) { if (perMonitor) { SessionData.setMonitorCyclingMode(selectedScreen, value); @@ -197,6 +206,15 @@ Item { } } + SettingsToggleRow { + tab: "wallpaper" + tags: ["cycling", "subfolders", "recursive", "directories", "folders"] + settingKey: "wallpaperCyclingRecursive" + text: I18n.tr("Include subfolders") + checked: root.recursive + onToggled: toggled => root.setRecursive(toggled) + } + SettingsToggleRow { tab: "wallpaper" tags: ["cycling", "automatic", "random", "shuffle"] diff --git a/quickshell/Services/WallpaperCyclingService.qml b/quickshell/Services/WallpaperCyclingService.qml index b8a46bf66..6c995f5c6 100644 --- a/quickshell/Services/WallpaperCyclingService.qml +++ b/quickshell/Services/WallpaperCyclingService.qml @@ -139,8 +139,9 @@ Singleton { }, null); } - function findCommand(wallpaperDir) { - return ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" -o -iname "*.svg" \\) 2>/dev/null | sort`]; + function findCommand(wallpaperDir, recursive) { + const depthArg = recursive ? "" : "-maxdepth 1 "; + return ["sh", "-c", `find -L "${wallpaperDir}" ${depthArg}-type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" -o -iname "*.svg" \\) 2>/dev/null | sort`]; } function monitorProcessFor(screenName) { @@ -158,25 +159,34 @@ Singleton { if (!currentWallpaper || currentWallpaper.startsWith("#")) return; let wallpaperDir; + let recursive = false; if (screenName) { const monitorSettings = SessionData.getMonitorCyclingSettings(screenName); + recursive = !!monitorSettings.recursive; if (monitorSettings.folderPath && currentWallpaper.startsWith(monitorSettings.folderPath + "/")) { wallpaperDir = monitorSettings.folderPath; } else { wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')); + if (recursive && wallpaperDir) + SessionData.setMonitorCyclingFolderPath(screenName, wallpaperDir); } } else { + recursive = !!SessionData.wallpaperCyclingRecursive; if (SessionData.wallpaperCyclingFolderPath && currentWallpaper.startsWith(SessionData.wallpaperCyclingFolderPath + "/")) { wallpaperDir = SessionData.wallpaperCyclingFolderPath; } else { wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')); + if (recursive && wallpaperDir) { + SessionData.wallpaperCyclingFolderPath = wallpaperDir; + SessionData.saveSettings(); + } } } if (screenName && monitorProcessComponent.status === Component.Ready) { var process = monitorProcessFor(screenName); - process.command = findCommand(wallpaperDir); + process.command = findCommand(wallpaperDir, recursive); process.targetScreenName = screenName; process.currentWallpaper = currentWallpaper; process.goToPrevious = goToPrevious; @@ -186,7 +196,7 @@ Singleton { } var globalProcess = goToPrevious ? prevCyclingProcess : cyclingProcess; - globalProcess.command = findCommand(wallpaperDir); + globalProcess.command = findCommand(wallpaperDir, recursive); globalProcess.targetScreenName = screenName || ""; globalProcess.currentWallpaper = currentWallpaper; globalProcess.revision = SessionData.wallpaperRequestRevision(screenName || ""); @@ -205,16 +215,19 @@ Singleton { if (!folderPath) return; + let recursive = false; if (screenName) { SessionData.setMonitorCyclingFolderPath(screenName, folderPath); + recursive = !!SessionData.getMonitorCyclingSettings(screenName).recursive; } else { SessionData.wallpaperCyclingFolderPath = folderPath; SessionData.saveSettings(); + recursive = !!SessionData.wallpaperCyclingRecursive; } if (screenName && monitorProcessComponent.status === Component.Ready) { var process = monitorProcessFor(screenName); - process.command = findCommand(folderPath); + process.command = findCommand(folderPath, recursive); process.targetScreenName = screenName; process.currentWallpaper = ""; process.goToPrevious = false; @@ -224,7 +237,7 @@ Singleton { } var globalProcess = cyclingProcess; - globalProcess.command = findCommand(folderPath); + globalProcess.command = findCommand(folderPath, recursive); globalProcess.targetScreenName = screenName || ""; globalProcess.currentWallpaper = ""; globalProcess.goToPrevious = false; diff --git a/quickshell/tests/session-monitor-cycling.test.mjs b/quickshell/tests/session-monitor-cycling.test.mjs index 8d4ec531f..12d5146bd 100644 --- a/quickshell/tests/session-monitor-cycling.test.mjs +++ b/quickshell/tests/session-monitor-cycling.test.mjs @@ -4,7 +4,7 @@ import vm from "node:vm"; import test from "node:test"; const source = readFileSync(new URL("../Common/SessionData.qml", import.meta.url), "utf8"); -const setters = ["setMonitorCyclingEnabled", "setMonitorCyclingRandom", "setMonitorCyclingMode", "setMonitorCyclingInterval", "setMonitorCyclingTime", "setMonitorCyclingFolderPath"]; +const setters = ["setMonitorCyclingEnabled", "setMonitorCyclingRandom", "setMonitorCyclingRecursive", "setMonitorCyclingMode", "setMonitorCyclingInterval", "setMonitorCyclingTime", "setMonitorCyclingFolderPath"]; const helpers = ["getMonitorCyclingSettings", "_findMonitorValue", "_screenByName", "updateMonitorCyclingSetting"]; function session(initial) { @@ -38,7 +38,7 @@ test("a setter writes its field under the display identifier and keeps other scr const result = plain(context.monitorCyclingSettings); assert.deepEqual(Object.keys(result).sort(), ["HDMI-A-1", "LG 27"]); assert.deepEqual(result["HDMI-A-1"], { enabled: true, interval: 10 }); - assert.deepEqual(result["LG 27"], { enabled: false, random: false, mode: "interval", interval: 60, time: "06:00", folderPath: "" }); + assert.deepEqual(result["LG 27"], { enabled: false, random: false, recursive: false, mode: "interval", interval: 60, time: "06:00", folderPath: "" }); assert.equal(saved(), 1); }); diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 122481fb9..b4ddf310c 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -16039,6 +16039,34 @@ ], "parentLabel": "Wallpaper & colors" }, + { + "section": "wallpaperCyclingRecursive", + "label": "Include subfolders", + "tabIndex": 52, + "category": "Automatic cycling", + "keywords": [ + "automatic", + "background", + "bg", + "colors", + "colour", + "colours", + "cycling", + "desktop", + "directories", + "folders", + "hue", + "image", + "include", + "palette", + "picture", + "recursive", + "subfolders", + "tint", + "wallpaper" + ], + "parentLabel": "Wallpaper & colors" + }, { "section": "wallpaperCyclingInterval", "label": "Interval",