// 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. pragma Singleton import Quickshell import Quickshell.Hyprland import QtQuick // Everything that touches Hyprland lives here, so the filter, the sort and // the dispatch syntax are in one place rather than spread across the UI. Singleton { id: root // Hyprland.toplevels reads 0 until this is called. function refresh() { Hyprland.refreshToplevels(); } // Windows on a special: workspace are the scratchpad, which is reached by // its own bind and is not something to tab to. Windows on other // workspaces are kept whatever their visibility: jumping to another // desktop is the point. readonly property var windows: { const out = []; for (const t of Hyprland.toplevels.values) { const ws = t.workspace; if (!ws || String(ws.name).startsWith("special:")) continue; out.push(t); } // Lowest focusHistoryID is the most recently used. HyprlandToplevel // does not expose it as a property, only inside the raw hyprctl // object, where a missing one sorts last rather than as 0. out.sort((a, b) => root.focusOrder(a) - root.focusOrder(b)); return out; } function focusOrder(toplevel) { const o = toplevel.lastIpcObject; const n = o ? Number(o.focusHistoryID) : NaN; return isNaN(n) ? Number.MAX_SAFE_INTEGER : n; } // Dispatch arguments are evaluated as Lua on Hyprland 0.56.2, so the // documented-looking `focuswindow address:0x...` is a syntax error that // fails silently. This form is the one that works. // // HyprlandToplevel.address has no 0x prefix, while the dispatch needs one. function addressOf(toplevel) { const a = String(toplevel.address); return a.startsWith("0x") ? a : "0x" + a; } function focus(toplevel) { Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addressOf(toplevel)}" })`); } function closeWindow(toplevel) { Hyprland.dispatch(`hl.dsp.window.close({ window = "address:${addressOf(toplevel)}" })`); } // The desktop marker on a card is the same icon waybar's bar draws for that // workspace, so the switcher and the bar agree. waybar keeps them as CSS // background images by workspace number (styles/modules.css); the names are // repeated here and resolved through the active icon theme, because a // QML-side icon has to go by name. A named workspace, or one past eight, // has no icon and the card falls back to its number. readonly property var workspaceIcons: ({ 1: "web-browser", 2: "text-editor", 3: "utilities-terminal", 4: "network-server", 5: "document-edit", 6: "applications-graphics", 7: "internet-chat", 8: "input-gaming", }) function workspaceIcon(workspace) { if (!workspace) return ""; const n = parseInt(String(workspace.name), 10); if (isNaN(n)) return ""; return root.workspaceIcons[n] ?? ""; } }