// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Wayland import QtQuick Scope { id: root property string monitor: "DP-1" property bool open: false property string tab: "wallpaper" property string notice: "" // What the mock screens show. Hovering a thumbnail previews it on the // targeted screen; the other keeps whatever is currently set. property string hovering: "" readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] function show(which) { if (which) root.tab = which; root.notice = ""; // Schemes and wallpapers can both have changed since this shell // started, which for an autostarted one is the whole session. Udt.refresh(); Wallpapers.refresh(); root.open = true; } function close() { root.open = false; } function toggle(which) { root.open ? close() : show(which); } Connections { target: Udt function onApplied(scheme, ok, message) { // install.sh reloads nothing, so the panel has to say what is // still showing the old scheme rather than pretend it is done. root.notice = ok ? `${scheme} applied. Reload: hyprctl reload, waybar, kitty, conky. Qt and GTK apps need restarting.` : `failed: ${message}`; } } Connections { target: Wallpapers function onApplied(ok, message) { root.notice = ok ? "" : `wallpaper failed: ${message}`; if (ok) root.close(); } } // Quickshell exits once no window is visible, and this panel is closed // most of the time. See AGENTS.md. PanelWindow { visible: true implicitWidth: 1 implicitHeight: 1 color: "transparent" exclusionMode: ExclusionMode.Ignore mask: Region {} WlrLayershell.keyboardFocus: WlrKeyboardFocus.None } LazyLoader { active: root.open PanelWindow { id: win screen: root.screenObj anchors { top: true; left: true; right: true; bottom: true } color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-appearance" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive Rectangle { anchors.fill: parent color: "#000000" opacity: 0.5 MouseArea { anchors.fill: parent; onClicked: root.close() } } // Keys reach a focused item, never the window itself. Item { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() Keys.onPressed: event => { if (event.key === Qt.Key_Tab) { root.tab = root.tab === "theme" ? "wallpaper" : "theme"; event.accepted = true; } } } Rectangle { anchors.centerIn: parent width: win.width - 120 height: win.height - 100 radius: 14 color: Qt.alpha(Theme.base, 0.72) border.width: 1 border.color: Qt.alpha(Theme.text, 0.12) MouseArea { anchors.fill: parent } Column { anchors.fill: parent anchors.margins: 20 spacing: 16 Row { spacing: 8 Tab { text: "Wallpaper" selected: root.tab === "wallpaper" onClicked: root.tab = "wallpaper" } Tab { text: "Theme" selected: root.tab === "theme" onClicked: root.tab = "theme" } Item { width: 12; height: 1 } Text { anchors.verticalCenter: parent.verticalCenter text: "Tab switches · Esc closes" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay } } Text { visible: root.notice !== "" width: parent.width wrapMode: Text.Wrap text: root.notice font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } color: root.notice.indexOf("failed") === 0 ? Theme.red : Theme.green } Loader { width: parent.width height: parent.height - y sourceComponent: root.tab === "theme" ? themeTab : wallpaperTab } } } } } // --- theme ------------------------------------------------------------ Component { id: themeTab Flickable { contentHeight: col.implicitHeight clip: true Column { id: col width: parent.width spacing: 10 Repeater { model: Udt.names Rectangle { required property string modelData readonly property var sw: Udt.swatches[modelData] ?? ({}) readonly property bool isCurrent: Udt.current === modelData width: col.width implicitHeight: 96 radius: 10 color: hover.hovered ? Qt.alpha(Theme.surface, 0.75) : Qt.alpha(Theme.surface, 0.35) border.width: 1 border.color: isCurrent ? Qt.alpha(Theme.accent, 0.6) : "transparent" HoverHandler { id: hover } MouseArea { anchors.fill: parent enabled: !Udt.applying && !isCurrent cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: Udt.apply(modelData) } Row { anchors.fill: parent anchors.margins: 14 spacing: 16 // A mock of the desktop in that scheme's colours: // swatches alone say what the colours are, this // says how they sit together. Rectangle { anchors.verticalCenter: parent.verticalCenter width: 150; height: 68 radius: 6 color: sw.bg ?? Theme.base border.width: 1 border.color: Qt.alpha(sw.text ?? Theme.text, 0.15) clip: true Column { anchors.fill: parent anchors.margins: 7 spacing: 5 Row { spacing: 4 Rectangle { width: 26; height: 8; radius: 2 color: sw.accent ?? Theme.accent } Rectangle { width: 16; height: 8; radius: 2 color: sw.surface ?? Theme.surface } Rectangle { width: 16; height: 8; radius: 2 color: sw.surface ?? Theme.surface } } Rectangle { width: parent.width; height: 20; radius: 3 color: sw.surface ?? Theme.surface Rectangle { anchors.verticalCenter: parent.verticalCenter x: 5; width: 60; height: 5; radius: 2 color: Qt.alpha(sw.text ?? Theme.text, 0.75) } Rectangle { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 5 width: 22; height: 9; radius: 3 color: sw.accent ?? Theme.accent } } Row { spacing: 4 Rectangle { width: 9; height: 9; radius: 5; color: sw.green ?? Theme.green } Rectangle { width: 9; height: 9; radius: 5; color: sw.yellow ?? Theme.yellow } Rectangle { width: 9; height: 9; radius: 5; color: sw.red ?? Theme.red } } } } Column { anchors.verticalCenter: parent.verticalCenter spacing: 8 Row { spacing: 8 Text { text: modelData font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } color: Theme.text } Text { anchors.verticalCenter: parent.verticalCenter visible: isCurrent text: "current" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.accent } Text { anchors.verticalCenter: parent.verticalCenter visible: Udt.applying text: "applying…" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay } } Row { spacing: 5 Repeater { model: [sw.bg, sw.surface, sw.text, sw.accent, sw.green, sw.yellow, sw.red] Rectangle { required property var modelData visible: modelData !== undefined && modelData !== "" width: 22; height: 22; radius: 4 color: modelData ?? "transparent" border.width: 1 border.color: Qt.alpha(Theme.text, 0.1) } } } } } } } } } } // --- wallpaper -------------------------------------------------------- Component { id: wallpaperTab Row { spacing: 20 Column { width: parent.width - mockPane.width - parent.spacing height: parent.height spacing: 12 Row { spacing: 8 Text { anchors.verticalCenter: parent.verticalCenter text: "Set on" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Tab { text: "Horizontal" selected: Wallpapers.target === "H" onClicked: Wallpapers.target = "H" } Tab { text: "Vertical" selected: Wallpapers.target === "V" onClicked: Wallpapers.target = "V" } Text { anchors.verticalCenter: parent.verticalCenter text: Wallpapers.scanning ? "scanning…" : Wallpapers.applying ? "applying…" : Wallpapers.files.length + " wallpapers" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay } } GridView { width: parent.width height: parent.height - y clip: true cellWidth: Math.floor(width / Math.max(1, Math.floor(width / 300))) cellHeight: cellWidth * 9 / 16 + 26 // Several hundred files: only the visible thumbnails decode. cacheBuffer: cellHeight * 2 model: Wallpapers.files delegate: Item { required property string modelData width: GridView.view.cellWidth height: GridView.view.cellHeight Rectangle { readonly property bool staged: Wallpapers.pendingH === modelData || Wallpapers.pendingV === modelData anchors.fill: parent anchors.margins: 6 radius: 8 color: Qt.alpha(Theme.surface, thumbHover.hovered ? 0.8 : 0.3) border.width: staged ? 2 : 1 border.color: staged ? Theme.accent : thumbHover.hovered ? Qt.alpha(Theme.accent, 0.6) : "transparent" clip: true HoverHandler { id: thumbHover // Hovering previews this image on the mock // screen it would actually land on. onHoveredChanged: root.hovering = hovered ? modelData : (root.hovering === modelData ? "" : root.hovering) } MouseArea { anchors.fill: parent enabled: !Wallpapers.applying cursorShape: Qt.PointingHandCursor onClicked: Wallpapers.pick(modelData) } Column { anchors.fill: parent anchors.margins: 6 spacing: 4 Image { width: parent.width height: parent.height - 18 source: "file://" + modelData asynchronous: true cache: false fillMode: Image.PreserveAspectCrop // Decode at display size: full-resolution // wallpapers would be megabytes each in memory. sourceSize.width: 480 } Text { width: parent.width elide: Text.ElideMiddle text: Wallpapers.basename(modelData) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } color: Theme.subtext } } } } } } // The two screens as they sit on the desk: DP-3 rotated upright on // the left, DP-1 wide on the right, centred against each other the // way Hyprland has them. Hovering a thumbnail fills the screen it // would be set on; the other keeps what is on it now. Item { id: mockPane // A third of the panel rather than a fixed width, so the // preview keeps its share when the panel is resized. width: Math.round(parent.width * 0.33) height: parent.height // Divisor is the two panels side by side plus the gap; the // bezels and stands are drawn outside that, hence the margin. readonly property real scale: (width * 0.92) / (1080 + 2560 + 160) Column { anchors.centerIn: parent spacing: 14 Row { spacing: Math.round(160 * mockPane.scale) MockScreen { anchors.verticalCenter: parent.verticalCenter label: "DP-3" targeted: Wallpapers.target === "V" source: (targeted && root.hovering !== "") ? root.hovering : Wallpapers.shown("V") // DP-3 has transform=1, so it stands upright. panelWidth: Math.round(1080 * mockPane.scale) panelHeight: Math.round(1920 * mockPane.scale) } MockScreen { anchors.verticalCenter: parent.verticalCenter label: "DP-1" targeted: Wallpapers.target === "H" source: (targeted && root.hovering !== "") ? root.hovering : Wallpapers.shown("H") panelWidth: Math.round(2560 * mockPane.scale) panelHeight: Math.round(1080 * mockPane.scale) } } Text { anchors.horizontalCenter: parent.horizontalCenter width: mockPane.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap text: root.hovering !== "" ? Wallpapers.basename(root.hovering) : Wallpapers.dirty ? "click Apply to set" : "click a wallpaper to stage it" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 8 Tab { text: Wallpapers.applying ? "applying…" : "Apply" selected: Wallpapers.dirty && !Wallpapers.applying enabled: Wallpapers.dirty && !Wallpapers.applying onClicked: Wallpapers.apply() } Tab { text: "Reset" enabled: Wallpapers.dirty && !Wallpapers.applying onClicked: Wallpapers.clearPending() } } } } } } }