// 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 import "Expose.js" as Expose Scope { id: root property bool open: false property string monitor: "DP-1" readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] function toggle() { root.open = !root.open; } function close() { root.open = false; } // Hyprland.toplevels reads empty until it is asked, and a window opened // since the last showing would otherwise be missing from the grid. onOpenChanged: if (open) Windows.refresh(); // A focus dispatched while the overlay is up is accepted and then ignored: // the layer surface holds keyboard focus exclusively, and the compositor // will not move window focus out from under that grab. Closing in the same // turn does not help either, because close() only clears a property and the // surface survives until the frame after. So the target is remembered and // focused once the grab is really gone. property var pendingFocus: null function focusAfterClose(toplevel) { root.pendingFocus = toplevel; root.close(); focusTimer.restart(); } Timer { id: focusTimer interval: 60 onTriggered: { if (root.pendingFocus) Windows.focus(root.pendingFocus); root.pendingFocus = null; } } // A config with no visible window exits, reporting nothing. This 1x1 // click-through window is what holds the shell open while the overlay // is hidden, which is almost always. PanelWindow { anchors { top: true; left: 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: "keystone-window-switcher" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive // Keys reach a focused item, not a window: Keys.onEscapePressed on // the PanelWindow above never fires. This item is what listens. // Escape is the only key: the expose is a pointer feature. Item { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() Rectangle { anchors.fill: parent color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.82) MouseArea { anchors.fill: parent onClicked: root.close() } // Nothing to switch to. A full-screen dim with nothing in // it reads as a hang, so it says so instead. Column { anchors.centerIn: parent spacing: 14 visible: Windows.windows.length === 0 Text { anchors.horizontalCenter: parent.horizontalCenter text: "Nothing here but hopes and dreams..." color: Theme.text font.family: Theme.fontFamily font.pixelSize: 26 } Text { anchors.horizontalCenter: parent.horizontalCenter text: "go create something beautiful!" color: Theme.overlay font.family: Theme.fontFamily font.pixelSize: 32 } } // The scatter. Recomputed whenever the window list or a // window's size changes, so closing one moves the rest. Item { id: scatter anchors.fill: parent // Real sizes come from the raw hyprctl object. A window // that has none yet stands in at 1280x800 until it does. readonly property var rects: Expose.layout( Windows.windows.map(t => { const o = t.lastIpcObject; const s = o && o.size; return s ? { w: s[0], h: s[1] } : { w: 1280, h: 800 }; }), width, height) // ponytail: a fade, not a flight from each window's real // position; add the flight if the fade feels abrupt. opacity: 0 Component.onCompleted: opacity = 1 Behavior on opacity { NumberAnimation { duration: 150 } } Repeater { model: Windows.windows WindowCard { required property int index required property var modelData readonly property var rect: scatter.rects[index] ?? { x: 0, y: 0, w: 0, h: 0 } toplevel: modelData x: rect.x y: rect.y width: rect.w height: rect.h onActivated: root.focusAfterClose(modelData) // Closing is a side errand, so the overlay // stays open: a dismissal would mean reopening // to close a second window. The card goes when // Hyprland says it went. onCloseRequested: Windows.closeWindow(modelData) } } } } } } } }