diff --git a/hypr/lua/binds/shell.lua b/hypr/lua/binds/shell.lua index 18fd522..37014df 100644 --- a/hypr/lua/binds/shell.lua +++ b/hypr/lua/binds/shell.lua @@ -10,6 +10,14 @@ shell("lock", { key = mod .. " + CTRL + L", description = "lock screen" }) shell("power", { key = mod .. " + P", description = "power menu" }) shell("ask", { key = mod .. " + A", description = "ask claude" }) shell("dashboard", { key = mod .. " + D", description = "dashboard" }) +shell("overview", { key = mod .. " + W", description = "overview of every workspace" }) + +-- Alt+Tab: hold Alt, tap Tab through focus history, let go to switch. The +-- overview sees the release itself; the release bind is a backstop for the +-- case where Alt comes up before the overview has the keyboard. +shell("overviewNext", { key = "ALT + Tab", description = "switch windows" }) +shell("overviewPrev", { key = "ALT + SHIFT + Tab", description = "switch windows, backwards" }) +hl.bind("ALT + ALT_L", hl.dsp.exec_cmd("qs ipc call shell overviewCommit"), { release = true }) shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" }) shell("toggleStayAwake", { key = mod .. " + SHIFT + I", description = "keep the screen awake" }) shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" }) diff --git a/hypr/lua/binds/windows.lua b/hypr/lua/binds/windows.lua index 87c05e6..a64357a 100644 --- a/hypr/lua/binds/windows.lua +++ b/hypr/lua/binds/windows.lua @@ -18,8 +18,10 @@ end -- ------------------------------------------------------------------- groups hl.bind(mod .. " + n", hl.dsp.group.toggle()) -hl.bind("ALT + Tab", hl.dsp.group.next()) --- hl.bind("ALT + Tab", hl.dsp.group.prev()) +-- Alt+Tab belongs to the overview (lua/binds/shell.lua), so tabs in a group +-- cycle on Super+Tab. +hl.bind(mod .. " + Tab", hl.dsp.group.next(), { description = "next window in group" }) +hl.bind(mod .. " + SHIFT + Tab", hl.dsp.group.prev(), { description = "previous window in group" }) -- -------------------------------------------------------------------- mouse hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) diff --git a/quickshell/README.md b/quickshell/README.md index ba1fe23..cf69553 100644 --- a/quickshell/README.md +++ b/quickshell/README.md @@ -66,6 +66,7 @@ bar/ the two surfaces and their modules popouts/ hover panels hanging off rail and bar modules overlays/ power menu, notification toasts, volume/brightness OSD lock/ session lock +overview/ every monitor, workspace and window at once backdrop/ the wallpaper, drawn by the shell assets/ halftone and stripe tiles, generated with ImageMagick ``` @@ -144,6 +145,9 @@ Set in `~/.config/hypr/lua/binds/shell.lua`: - `Super+Ctrl+L` — lock - `Super+P` — power menu - `Super+Shift+N` — toggle do not disturb +- `Alt+Tab` / `Alt+Shift+Tab` — overview, walking focus history; let go of Alt to switch +- `Super+W` — overview, without the hold +- `Super+Tab` / `Super+Shift+Tab` — next / previous window in a group - `Super+Shift+I` — stay awake (no dim, lock or screen-off until toggled back) - `Super+Ctrl+Shift+N` — clear notifications - `Super+A` — ask panel @@ -215,6 +219,27 @@ NetworkManager for Wi-Fi, UPower for battery, MPRIS for media, and Quickshell's own notification server. These are event-driven, so the bar and OSD react the instant a volume key is pressed instead of on the next poll. +## Overview + +`overview/Overview.qml`. Each monitor is a row of small portholes, one per +workspace at that monitor's aspect ratio, with live captures of the windows +where they sit (`ScreencopyView`, so windows on hidden workspaces show too). +The monitor's name heads its row once there is more than one. A group shows +the member in front with a `1/3` tag; picking a hidden member puts it in +front. + +`Alt+Tab` opens it with the previous window picked, more Tabs walk back +through focus history, and letting go of Alt switches. Hyprland keeps the +Alt+Tab bind while the overview has the keyboard, so those presses arrive +over IPC (`overviewNext` / `overviewPrev`); the Alt release comes to the +overview itself, with a release bind on `ALT_L` as a backstop. Hover picks +only once the pointer moves, so a resting pointer cannot override the +keyboard. Enter, a click, or `1`–`9` for a workspace also go; Escape or a +click on the dark cancels. + +Focus is dispatched a moment after the overview lets go of the keyboard: +Hyprland ignores focus changes while a layer holds it exclusively. + ## Wallpaper `backdrop/Backdrop.qml` draws it on the background layer; there is no diff --git a/quickshell/overview/Overview.qml b/quickshell/overview/Overview.qml new file mode 100644 index 0000000..66aee30 --- /dev/null +++ b/quickshell/overview/Overview.qml @@ -0,0 +1,366 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import "root:/config" +import "root:/components" +import "root:/services" + +// Every monitor, every workspace, every window, at once. Alt+Tab opens it on +// the focused screen with the previous window picked; more Tabs walk back +// through focus history, and letting go of Alt goes there. Hover and click +// work too, as do Enter, Escape and the workspace digits. +// +// It is a scene, like the power menu: no panel, the desktop dims, and each +// monitor is laid out as a row of small portholes — its workspaces, at its +// own aspect ratio, with live captures of the windows where they sit. The +// picked window is ringed in the light and named underneath. +PanelWindow { + id: root + + required property var modelData + screen: modelData + + readonly property bool open: Actions.overviewOpen + && Actions.overviewScreen === modelData.name + visible: root.open || fade.running + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "takemi-overview" + WlrLayershell.keyboardFocus: root.open + ? WlrKeyboardFocus.Exclusive + : WlrKeyboardFocus.None + exclusionMode: ExclusionMode.Ignore + anchors { top: true; bottom: true; left: true; right: true } + color: "transparent" + + // ----------------------------------------------------------------- model + // Rebuilt from Hyprland's IPC snapshots on open and whenever windows move + // while open. Plain arrays rather than live models: everything here is a + // picture of one moment. + property var monitors: [] // [{ mon, ipc, workspaces: [{ ws, windows: [...] }] }] + property var mru: [] // window addresses, most recent first + property string picked: "" // address of the picked window + + readonly property var pickedWindow: { + for (const m of root.monitors) + for (const w of m.workspaces) + for (const win of w.windows) + if (win.address === root.picked) return win; + return null; + } + + function rebuild() { + const mons = Hyprland.monitors.values.slice() + .filter(m => m.lastIpcObject && m.lastIpcObject.width) + .sort((a, b) => a.lastIpcObject.x - b.lastIpcObject.x); + const all = []; + const out = []; + for (const mon of mons) { + const mi = mon.lastIpcObject; + const wss = Hyprland.workspaces.values + .filter(ws => ws.monitor === mon) + .filter(ws => ws.id > 0 || ws.toplevels.values.length > 0) + .sort((a, b) => (a.id < 0) - (b.id < 0) || a.id - b.id); + const rows = []; + for (const ws of wss) { + const wins = []; + for (const tl of ws.toplevels.values) { + const ipc = tl.lastIpcObject; + if (!ipc || !ipc.at || !ipc.mapped) continue; + const win = { + toplevel: tl, + address: "0x" + String(tl.address).replace(/^0x/, ""), + x: ipc.at[0] - mi.x, y: ipc.at[1] - mi.y, + w: ipc.size[0], h: ipc.size[1], + title: ipc.title || "", + appClass: ipc.class || "", + hidden: ipc.hidden === true, + floating: ipc.floating === true, + group: (ipc.grouped || []).map(a => "0x" + String(a).replace(/^0x/, "")), + history: ipc.focusHistoryID ?? 999, + }; + wins.push(win); + all.push(win); + } + // Tiled first, floating on top, as Hyprland draws them. + wins.sort((a, b) => a.floating - b.floating); + rows.push({ ws: ws, windows: wins }); + } + out.push({ mon: mon, ipc: mi, workspaces: rows }); + } + root.monitors = out; + root.mru = all.sort((a, b) => a.history - b.history).map(w => w.address); + if (!root.touched || root.mru.indexOf(root.picked) < 0) + root.picked = root.mru.length > 1 ? root.mru[1] : (root.mru[0] ?? ""); + } + + function step(delta: int) { + root.touched = true; + const n = root.mru.length; + if (n === 0) return; + const at = Math.max(0, root.mru.indexOf(root.picked)); + root.picked = root.mru[(at + delta + n) % n]; + } + + // Hyprland will not move focus while a layer holds the keyboard + // exclusively, so let go of it first and dispatch a moment later. + property string pendingDispatch: "" + + function commit() { + root.pendingDispatch = root.picked !== "" + ? "hl.dsp.focus({ window = \"address:" + root.picked + "\" })" + : ""; + Actions.closeOverview(); + if (root.pendingDispatch !== "") release.restart(); + } + + function goToWorkspace(id: int) { + root.pendingDispatch = "hl.dsp.focus({ workspace = " + id + " })"; + Actions.closeOverview(); + release.restart(); + } + + Timer { + id: release + interval: 40 + onTriggered: Hyprland.dispatch(root.pendingDispatch) + } + + // Whether the pointer has moved since the overview opened. A pointer that + // happens to rest over a card would otherwise pick that window the moment + // the cards appear under it, overriding Alt+Tab's choice. + property bool pointerLive: false + property point pointerStart: Qt.point(-1, -1) + + // Whether the pick came from a person rather than from the first snapshot. + property bool touched: false + + onOpenChanged: { + if (root.open) { + PopupCoordinator.dismissAll(); + Hyprland.refreshMonitors(); + Hyprland.refreshWorkspaces(); + Hyprland.refreshToplevels(); + root.picked = ""; + root.touched = false; + root.pointerLive = false; + root.pointerStart = Qt.point(-1, -1); + root.rebuild(); + // The IPC snapshots land a moment after the refresh request. + settle.restart(); + keys.forceActiveFocus(); + } + } + + Timer { + id: settle + interval: 60 + onTriggered: root.rebuild() + } + + Connections { + target: Actions + function onOverviewStepped(delta) { if (root.open) root.step(delta); } + function onOverviewCommitted() { if (root.open) root.commit(); } + } + + Connections { + target: Hyprland + enabled: root.open + function onRawEvent(event) { + const n = event.name; + if (n === "openwindow" || n === "closewindow" || n === "movewindowv2" + || n === "changefloatingmode" || n === "fullscreen") { + Hyprland.refreshToplevels(); + settle.restart(); + } + } + } + + // ------------------------------------------------------------- backdrop + Rectangle { + anchors.fill: parent + color: Theme.ink + opacity: root.open ? 0.92 : 0 + + Behavior on opacity { + NumberAnimation { id: fade; duration: Theme.durBase; easing.type: Easing.OutCubic } + } + + TapHandler { onTapped: Actions.closeOverview() } + } + + // ---------------------------------------------------------------- scene + Item { + id: keys + anchors.fill: parent + focus: true + + HoverHandler { + onPointChanged: { + const p = point.position; + if (root.pointerStart.x < 0) { root.pointerStart = p; return; } + if (Math.abs(p.x - root.pointerStart.x) + Math.abs(p.y - root.pointerStart.y) > 6) + root.pointerLive = true; + } + } + + Keys.onPressed: event => { + const k = event.key; + if (k === Qt.Key_Escape) Actions.closeOverview(); + else if (k === Qt.Key_Return || k === Qt.Key_Enter || k === Qt.Key_Space) root.commit(); + else if (k === Qt.Key_Tab || k === Qt.Key_Right || k === Qt.Key_Down + || k === Qt.Key_L || k === Qt.Key_J) root.step(1); + else if (k === Qt.Key_Backtab || k === Qt.Key_Left || k === Qt.Key_Up + || k === Qt.Key_H || k === Qt.Key_K) root.step(-1); + else if (k >= Qt.Key_1 && k <= Qt.Key_9) root.goToWorkspace(k - Qt.Key_0); + else return; + event.accepted = true; + } + + // Alt+Tab is a hold: letting go of Alt is the choice. + Keys.onReleased: event => { + if (event.key === Qt.Key_Alt && Actions.overviewHold) { + event.accepted = true; + root.commit(); + } + } + } + + Column { + id: scene + anchors.centerIn: parent + spacing: Theme.padXL + opacity: root.open ? 1 : 0 + + transform: Translate { + y: root.open ? 0 : 10 + + Behavior on y { + NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic } + } + } + + Behavior on opacity { + NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic } + } + + // The widest a row of cards may be, and how tall all rows together. + readonly property real maxRowWidth: root.width * 0.86 + readonly property real maxRowsHeight: root.height * 0.62 + + // One scale for every card on every monitor, so a window is the same + // size wherever it sits: the largest that fits both the widest row and + // the summed heights. + readonly property real k: { + let k = 0.25; + let totalH = 0; + for (const m of root.monitors) { + const w = m.ipc.width / m.ipc.scale; + const h = m.ipc.height / m.ipc.scale; + const n = Math.max(1, m.workspaces.length); + k = Math.min(k, (scene.maxRowWidth - (n - 1) * Theme.padL) / (n * w)); + totalH += h; + } + if (totalH > 0) k = Math.min(k, scene.maxRowsHeight / totalH); + return Math.max(0.05, k); + } + + Repeater { + model: root.monitors + + Column { + id: monRow + required property var modelData + readonly property real logicalW: modelData.ipc.width / modelData.ipc.scale + readonly property real logicalH: modelData.ipc.height / modelData.ipc.scale + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.padS + + // The monitor's name only matters once there is more than one. + Text { + visible: root.monitors.length > 1 + text: (monRow.modelData.mon.name + " · " + + Math.round(monRow.logicalW) + "×" + Math.round(monRow.logicalH)).toUpperCase() + color: Theme.muted + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering + } + + Row { + spacing: Theme.padL + + Repeater { + model: monRow.modelData.workspaces + + WorkspaceCard { + required property var modelData + ws: modelData.ws + windows: modelData.windows + k: scene.k + logicalW: monRow.logicalW + logicalH: monRow.logicalH + current: monRow.modelData.mon.activeWorkspace === modelData.ws + || (modelData.ws.id < 0 && monRow.modelData.ipc.specialWorkspace?.id === modelData.ws.id) + picked: root.picked + onPick: address => { + if (!root.pointerLive) return; + root.touched = true; + root.picked = address; + } + onActivate: address => { root.picked = address; root.commit(); } + onActivateWorkspace: id => root.goToWorkspace(id) + } + } + } + } + } + + // ------------------------------------------------------------ caption + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: 4 + width: scene.maxRowWidth * 0.7 + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: root.pickedWindow ? root.pickedWindow.title : "No windows" + color: Theme.accent + elide: Text.ElideRight + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsTitle + font.weight: Font.Light + renderType: Text.NativeRendering + } + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + visible: root.pickedWindow !== null + text: root.pickedWindow + ? (root.pickedWindow.appClass + " · workspace " + + (root.pickedWindow.toplevel.workspace?.name ?? "?")).toUpperCase() + : "" + color: Theme.muted + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering + } + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "TAB NEXT ENTER GO 1–9 WORKSPACE ESC CANCEL" + color: Theme.overlay + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + font.letterSpacing: 2 + renderType: Text.NativeRendering + } + } +} diff --git a/quickshell/overview/WorkspaceCard.qml b/quickshell/overview/WorkspaceCard.qml new file mode 100644 index 0000000..e9bad9e --- /dev/null +++ b/quickshell/overview/WorkspaceCard.qml @@ -0,0 +1,191 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import "root:/config" +import "root:/services" + +// One workspace in the overview: a small porthole at its monitor's aspect +// ratio, the wallpaper faint behind it, and its windows as live captures where +// they sit. Under it, the workspace's number in the bar's pill when it is the +// one on screen. +Item { + id: card + + required property HyprlandWorkspace ws + required property var windows + required property real k // logical px -> card px + required property real logicalW + required property real logicalH + property bool current: false + property string picked: "" + + signal pick(string address) + signal activate(string address) + signal activateWorkspace(int id) + + readonly property bool special: card.ws.id < 0 + // The picked window, if it is one of the hidden members of a group here: + // the group's slot shows it instead of the member that is in front. + readonly property var pickedGroup: { + for (const w of card.windows) + if (w.address === card.picked && w.group.length > 1) return w.group; + return []; + } + + implicitWidth: Math.round(card.logicalW * card.k) + implicitHeight: porthole.height + label.anchors.topMargin + label.height + + Rectangle { + id: porthole + width: card.implicitWidth + height: Math.round(card.logicalH * card.k) + radius: Theme.radiusM + color: Theme.mantle + border.width: 1 + border.color: card.current ? Theme.outline : Theme.rim + clip: true + + Image { + anchors.fill: parent + anchors.margins: 1 + source: Wallpaper.source + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectCrop + asynchronous: true + opacity: 0.35 + } + + // Click on bare wallpaper: go to the workspace itself. + TapHandler { + onTapped: if (!card.special) card.activateWorkspace(card.ws.id) + } + + Repeater { + model: card.windows + + Rectangle { + id: tile + required property var modelData + readonly property bool isPicked: tile.modelData.address === card.picked + // A group draws one member: the picked one if it is in this + // group, otherwise whichever Hyprland has in front. + visible: card.pickedGroup.length > 0 && card.pickedGroup.indexOf(tile.modelData.address) >= 0 + ? tile.isPicked + : !tile.modelData.hidden + + x: Math.round(tile.modelData.x * card.k) + y: Math.round(tile.modelData.y * card.k) + width: Math.max(8, Math.round(tile.modelData.w * card.k)) + height: Math.max(8, Math.round(tile.modelData.h * card.k)) + z: tile.isPicked ? 2 : (tile.modelData.floating ? 1 : 0) + radius: Theme.radiusS + color: Theme.surface + clip: true + + // The app's icon, under the capture, for the moment before the + // first frame lands or for a window that cannot be captured. + Image { + anchors.centerIn: parent + width: Math.min(32, parent.width * 0.4) + height: width + sourceSize: Qt.size(width, height) + source: { + const e = DesktopEntries.heuristicLookup(tile.modelData.appClass); + return e && e.icon ? Quickshell.iconPath(e.icon, true) : ""; + } + visible: !capture.hasContent + } + + ScreencopyView { + id: capture + anchors.fill: parent + captureSource: tile.modelData.toplevel.wayland + live: true + constraintSize: Qt.size(tile.width, tile.height) + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.width: tile.isPicked ? 2 : 1 + border.color: tile.isPicked ? Theme.accent : Theme.alpha(Theme.ink, 0.6) + + Behavior on border.color { + ColorAnimation { duration: Theme.durFast } + } + } + + // Everything else sinks back a little. + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Theme.ink + opacity: tile.isPicked ? 0 : 0.28 + + Behavior on opacity { + NumberAnimation { duration: Theme.durFast } + } + } + + // How many windows share this slot. + Rectangle { + visible: tile.modelData.group.length > 1 + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 4 + width: groupText.implicitWidth + 8 + height: groupText.implicitHeight + 2 + radius: height / 2 + color: Theme.alpha(Theme.ink, 0.75) + + Text { + id: groupText + anchors.centerIn: parent + text: (tile.modelData.group.indexOf(tile.modelData.address) + 1) + + "/" + tile.modelData.group.length + color: Theme.subtext + font.family: Theme.fontMono + font.pixelSize: Theme.fsMicro + renderType: Text.NativeRendering + } + } + + HoverHandler { + onHoveredChanged: if (hovered) card.pick(tile.modelData.address) + // Hovered from the start, then the pointer moves: still a pick. + onPointChanged: if (hovered) card.pick(tile.modelData.address) + } + + TapHandler { + onTapped: card.activate(tile.modelData.address) + } + } + } + } + + // The workspace's name: the focused one wears it in the bar's light pill, + // the rest in grey. + Rectangle { + id: label + anchors.top: porthole.bottom + anchors.topMargin: Theme.padS + anchors.horizontalCenter: porthole.horizontalCenter + width: Math.max(height * 1.6, labelText.implicitWidth + 14) + height: labelText.implicitHeight + 4 + radius: height / 2 + color: card.current ? Theme.accent : "transparent" + + Text { + id: labelText + anchors.centerIn: parent + text: card.special ? card.ws.name.replace(/^special:/, "") : String(card.ws.id) + color: card.current ? Theme.ink : Theme.muted + font.family: card.special ? Theme.fontMono : Theme.fontDisplay + font.pixelSize: Theme.fsSmall + font.weight: Theme.weightDisplay + renderType: Text.NativeRendering + } + } +} diff --git a/quickshell/services/Actions.qml b/quickshell/services/Actions.qml index e8bb8fa..61ec1c8 100644 --- a/quickshell/services/Actions.qml +++ b/quickshell/services/Actions.qml @@ -19,6 +19,9 @@ Singleton { property bool launcherOpen: false property bool askOpen: false property bool dashboardOpen: false + property bool overviewOpen: false + // Opened by Alt+Tab, so letting go of Alt picks the window. + property bool overviewHold: false // Seed text for the next launcher open. Lets a keybind or a script hand the // launcher a query it did not have to type — `qs ipc call shell prompt @@ -40,6 +43,9 @@ Singleton { // …and for the dashboard drawer. property string dashboardScreen: "" + // …and for the overview. + property string overviewScreen: "" + // When the dashboard was last closed from outside. A click on the bar's // clock first clears the drawer's focus grab, which closes it, and then // lands on the clock, which would toggle it straight back open. A toggle @@ -51,6 +57,8 @@ Singleton { property string askSeed: "" signal osdRequested(string kind) + signal overviewStepped(int delta) + signal overviewCommitted() function togglePowerMenu() { if (!root.powerMenuOpen) @@ -88,6 +96,29 @@ Singleton { root.dashboardClosedAt = Date.now(); } + function toggleOverview() { + if (!root.overviewOpen) { + root.overviewHold = false; + root.overviewScreen = root.focusedScreenName(); + } + root.overviewOpen = !root.overviewOpen; + } + function closeOverview() { root.overviewOpen = false; } + + // Alt+Tab and Alt+Shift+Tab. The first press opens the overview with the + // previous window picked; Hyprland keeps the bind while it is open, so + // later presses arrive here too and walk the focus history. + function overviewStep(delta: int) { + if (!root.overviewOpen) { + root.overviewHold = true; + root.overviewScreen = root.focusedScreenName(); + root.overviewOpen = true; + if (delta < 0) root.overviewStepped(-2); + return; + } + root.overviewStepped(delta); + } + function toggleAsk() { if (!root.askOpen) { root.askSeed = ""; @@ -164,6 +195,10 @@ Singleton { function toggleDnd(): void { Notifs.toggleDnd(); } function clearNotifications(): void { Notifs.clearAll(); } function toggleStayAwake(): void { Idle.toggleStayAwake(); } + function overview(): void { root.toggleOverview(); } + function overviewNext(): void { root.overviewStep(1); } + function overviewPrev(): void { root.overviewStep(-1); } + function overviewCommit(): void { if (root.overviewHold) root.overviewCommitted(); } function wallpaper(path: string): void { Wallpaper.set(path); } } } diff --git a/quickshell/shell.qml b/quickshell/shell.qml index ad7fdb1..16cea91 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -7,6 +7,7 @@ import "root:/bar" import "root:/launcher" import "root:/dashboard" import "root:/ask" +import "root:/overview" import "root:/overlays" import "root:/lock" @@ -18,6 +19,7 @@ import "root:/lock" // popouts/ hover panels hanging off bar modules // launcher/ the application launcher // dashboard/ the drawer that drops from the bar +// overview/ every monitor, workspace and window at once (Alt+Tab) // ask/ the question panel, dropped from the top edge // overlays/ power menu, notification toasts, OSD // lock/ session lock @@ -84,6 +86,12 @@ ShellRoot { AskPanel {} } + Variants { + model: Quickshell.screens + + Overview {} + } + Variants { model: Quickshell.screens