// WallpaperPopup.qml - A popup for browsing and setting wallpapers from /usr/share/wallpapers import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland import ".." PanelWindow { id: root required property var screen property bool logicalOpen: GlobalStates.popups.active === "wallpaper" property bool _keepVisible: false visible: logicalOpen || _keepVisible onLogicalOpenChanged: { if (logicalOpen) { closeTimer.stop() _keepVisible = false if (wallpaperModel.count === 0) scanProc.running = true openY.start() openOpacity.start() focusTimer.start() } else { _keepVisible = true closeY.start() closeOpacity.start() closeTimer.restart() } } Timer { id: closeTimer; interval: 280; onTriggered: root._keepVisible = false } Timer { id: focusTimer; interval: 50; onTriggered: grid.forceActiveFocus() } WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-popup-wallpaper" WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand anchors { top: true; left: true; right: true; bottom: true } color: Qt.rgba(0, 0, 0, 0) MouseArea { anchors.fill: parent; onClicked: GlobalStates.popups.close() } NumberAnimation { id: openY; target: box; property: "y"; to: 1; duration: 250; easing.type: Easing.OutCubic } NumberAnimation { id: closeY; target: box; property: "y"; to: -340; duration: 200; easing.type: Easing.InCubic } NumberAnimation { id: openOpacity; target: box; property: "opacity"; to: Theme.popupOpacity; duration: 200 } NumberAnimation { id: closeOpacity; target: box; property: "opacity"; to: 0.0; duration: 200 } // ── Named-pipe IPC ──────────────────────────────────────── // Trigger from Hyprland keybind: echo 1 > /tmp/qs-wallpaper-ipc // Trigger from another QML popup: Exec.run(["bash", "-c", "echo 1 > /tmp/qs-wallpaper-ipc"]) Process { id: ipcReader running: true command: ["bash", "-c", "rm -f /tmp/qs-wallpaper-ipc && " + "mkfifo /tmp/qs-wallpaper-ipc && " + "while true; do cat /tmp/qs-wallpaper-ipc; done"] stdout: SplitParser { onRead: () => GlobalStates.popups.toggle("wallpaper", 0) } onExited: running = true } // ── Wallpaper scanner ───────────────────────────────────── ListModel { id: wallpaperModel } Process { id: scanProc running: false command: ["bash", "-c", "find /usr/share/wallpapers -type f " + "\\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) " + "| sort"] stdout: SplitParser { onRead: (line) => { var p = line.trim() if (p) wallpaperModel.append({ path: p }) } } } Process { id: setProc; running: false } // ── Popup box ───────────────────────────────────────────── Rectangle { id: box // anchorX = 0 → centered; anchorX > 0 → centered under widget x: { var ax = GlobalStates.popups.anchorX var cx = ax > 0 ? ax - width / 2 : parent.width / 2 - width / 2 return Math.max(4, Math.min(parent.width - width - 4, cx)) } y: -340 opacity: 0.0 width: 324 height: 500 color: Theme.popupBackground radius: Theme.radius * 2 clip: true border.color: Theme.popupBorderColor border.width: Theme.popupBorderWidth MouseArea { anchors.fill: parent } Text { anchors.centerIn: parent visible: wallpaperModel.count === 0 text: "Scanning..." font { family: Theme.fontSans; pixelSize: Theme.fontSize } color: Theme.pillText opacity: 0.5 } GridView { id: grid anchors { fill: parent; margins: 8 } clip: true cellWidth: Math.floor(width / 2) cellHeight: 104 focus: true keyNavigationEnabled: true currentIndex: 0 Keys.onReturnPressed: { var item = wallpaperModel.get(grid.currentIndex) if (item) { setProc.command = ["waypaper", "--wallpaper", item.path] setProc.running = true GlobalStates.popups.close() } } Keys.onEscapePressed: GlobalStates.popups.close() model: wallpaperModel highlight: Rectangle { z: 2 radius: Theme.radius + 1 color: "transparent" border.color: Theme.accent ?? "#cba6f7" border.width: 2 } highlightFollowsCurrentItem: true highlightMoveDuration: 80 delegate: Item { required property string path required property int index width: grid.cellWidth - 4 height: 100 Rectangle { anchors { fill: parent; margins: 2 } radius: Theme.radius color: "black" clip: true Image { anchors.fill: parent source: "file://" + path fillMode: Image.PreserveAspectCrop sourceSize: Qt.size(152, 96) smooth: true asynchronous: true opacity: status === Image.Ready ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 150 } } } Rectangle { anchors.fill: parent radius: Theme.radius color: hover.containsMouse ? Qt.rgba(1, 1, 1, 0.2) : "transparent" Behavior on color { ColorAnimation { duration: 100 } } } MouseArea { id: hover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { grid.currentIndex = index setProc.command = ["waypaper", "--wallpaper", path] setProc.running = true GlobalStates.popups.close() } onEntered: grid.currentIndex = index } } } } } }