aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/Windows.qml
blob: fb582ad4cba0e0c68e3841c41dc03d347952a227 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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: "utilities-terminal",
        3: "text-editor",
        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] ?? "";
    }
}