Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Niri.qml

Raw
import QtQuick
import Quickshell
import Quickshell.Io
import "niri.js" as NiriState

Item {
    id: root

    readonly property bool available: String(Quickshell.env("NIRI_SOCKET") || "").length > 0
    property bool ready: false
    property bool windowsInitialized: false
    property bool workspacesInitialized: false
    property bool streamFailed: false
    property int retryDelay: 250
    property string lastError: ""
    property string outputError: ""
    property bool outputsPending: false
    property bool outputsFinished: false
    signal screenshotCaptured(var path)
    property var workspaces: []
    readonly property var visibleWorkspaces: displayWorkspaces(workspaces)
    property var windows: []
    property var outputs: ({})
    property var focusedWindow: null
    property string overviewState: "unknown"
    property string lastPickedColor: ""
    property var lastWindowAction: ({})
    property var actionQueue: []
    readonly property var focusedWindowRecord: windowById(focusedWindow)
    readonly property string focusedTitle: focusedWindowRecord ? (focusedWindowRecord.title || "") : ""
    readonly property string focusedAppId: focusedWindowRecord ? (focusedWindowRecord.app_id || focusedWindowRecord.appId || "") : ""
    readonly property string focusedOutputName: focusedOutputNameFromWorkspaces(workspaces)
    readonly property string keybindConfigPath: {
        const xdg = String(Quickshell.env("XDG_CONFIG_HOME") || "");
        const home = String(Quickshell.env("HOME") || "");
        return (xdg.length > 0 ? xdg : home + "/.config") + "/niri/cfg/keybinds.kdl";
    }
    property var workspaceShortcuts: ({})

    function refresh() {
        if (!available || query.running)
            return;

        reconnect.stop();
        resetStream();
        streamFailed = false;
        lastError = "";
        query.exec(["niri", "msg", "--json", "event-stream"]);
    }

    function resetStream() {
        ready = false;
        windowsInitialized = false;
        workspacesInitialized = false;
        windows = [];
        workspaces = [];
        focusedWindow = null;
        overviewState = "unknown";
    }

    function streamDisconnected() {
        resetStream();
        if (lastError.length === 0)
            lastError = "event stream disconnected";
        if (!available || reconnect.running)
            return;
        reconnect.interval = retryDelay;
        retryDelay = Math.min(retryDelay * 2, 8000);
        reconnect.start();
    }

    function failStream(message) {
        lastError = message;
        ready = false;
        streamFailed = true;
        query.signal(9);
    }

    function refreshOutputs() {
        if (!available)
            return;
        if (outputQuery.running) {
            outputsPending = true;
            return;
        }
        outputsPending = false;
        outputsFinished = false;
        outputQuery.exec(["niri", "msg", "--json", "outputs"]);
    }

    function applyOutputs(text, code, status) {
        try {
            const inventory = NiriState.parseOutputs(text, code, status);
            if (JSON.stringify(outputs) !== JSON.stringify(inventory))
                outputs = inventory;
            outputError = "";
        } catch (error) {
            outputError = String(error);
        }
    }

    function action(args) {
        const list = Array.isArray(args) ? args : [String(args)];
        actionQueue = actionQueue.concat([["niri", "msg", "action"].concat(list.map(String))]);
        dispatchNextAction();
    }

    function dispatchNextAction() {
        if (actionProc.running || actionQueue.length === 0)
            return;
        const command = actionQueue[0];
        actionQueue = actionQueue.slice(1);
        // Process.exec terminates an already-running command; preserve rapid requests.
        actionProc.exec(command);
    }

    function screenshot(path) {
        action(["screenshot", "--path", path]);
    }

    function screenshotScreen(path) {
        action(["screenshot-screen", "--path", path, "--write-to-disk", "true"]);
    }

    function screenshotWindow(path) {
        action(["screenshot-window", "--path", path, "--write-to-disk", "true"]);
    }

    function pickColor() {
        lastPickedColor = "";
        colorProc.exec(["niri", "msg", "pick-color"]);
    }

    function focusWorkspace(ref) {
        action(["focus-workspace", String(ref)]);
    }

    function displayWorkspaces(items) {
        return (items || []).filter(workspace => {
            return workspace.name || workspace.is_focused || workspace.is_active || workspace.is_urgent || workspace.active_window_id !== null && workspace.active_window_id !== undefined;
        });
    }

    function focusedOutputNameFromWorkspaces(items) {
        const list = items || [];
        for (let i = 0; i < list.length; i++)
            if (list[i].is_focused)
                return String(list[i].output || "");

        return "";
    }

    function shortcutKey(sequence) {
        const parts = String(sequence || "").split("+");
        return parts.length > 0 ? parts[parts.length - 1] : "";
    }

    function parseWorkspaceShortcuts(text) {
        const map = ({});
        const re = /(?:^|\n)\s*([^\s{]+)[^{]*\{([^{}]*)\}/g;
        let match;
        while ((match = re.exec(String(text || ""))) !== null) {
            const focus = /\bfocus-workspace\s+(?:"([^"]+)"|([^\s}]+))/.exec(match[2]);
            if (!focus)
                continue;

            const name = String(focus[1] || focus[2] || "");
            if (name.length > 0 && !map[name])
                map[name] = shortcutKey(match[1]);
        }
        return map;
    }

    function reloadWorkspaceShortcuts() {
        workspaceShortcuts = parseWorkspaceShortcuts(keybindFile.text());
    }

    function workspaceShortcut(name) {
        return workspaceShortcuts[String(name || "")] || "";
    }

    // Group visible workspaces by output, each output's list idx-sorted, so the
    // single primary bar renders every monitor's workspaces in its own tray.
    // Pure: pass `items` for testing; defaults to the live workspace array.
    function groupWorkspaces(items) {
        const vis = displayWorkspaces(items === undefined ? workspaces : items);
        const byOutput = ({});
        const order = [];
        for (let i = 0; i < vis.length; i++) {
            const name = String(vis[i].output || "");
            if (!byOutput[name]) {
                byOutput[name] = [];
                order.push(name);
            }
            byOutput[name].push(Object.assign({}, vis[i], {
                "shortcut": workspaceShortcut(vis[i].name)
            }));
        }
        return order.map(name => {
            return {
                "output": name,
                "workspaces": byOutput[name].slice().sort((a, b) => Number(a.idx || 0) - Number(b.idx || 0))
            };
        });
    }

    // Installed niri's outputs request returns a name-keyed object.
    function outputList() {
        return Object.keys(outputs).map(name => outputs[name]);
    }

    // physical left-to-right order key for an output, from niri's logical x;
    // large fallback so unknown outputs sort last (stable, not hash order).
    function outputX(name) {
        const list = outputList();
        const want = String(name || "");
        for (let i = 0; i < list.length; i++) {
            if (String(list[i].name || "") === want) {
                const logical = list[i].logical || ({});
                return typeof logical.x === "number" ? logical.x : 1000000;
            }
        }
        return 1000000;
    }

    // live grouping for the bar, groups ordered by physical output position.
    function workspaceGroups() {
        return groupWorkspaces(workspaces).slice().sort((a, b) => outputX(a.output) - outputX(b.output));
    }

    // Local gate for the grouping logic (untestable on a single monitor): feed
    // a synthetic two-output array and assert grouping, per-tray idx sort, and
    // empty-trailing-workspace filtering. Drive via the niriSelfTest IPC.
    function selfTest() {
        const failures = [];
        const sample = [
            {
                "idx": 4,
                "output": "DP-2",
                "name": "browser"
            },
            {
                "idx": 5,
                "output": "DP-2",
                "name": "vault"
            },
            {
                "idx": 3,
                "output": "DP-1",
                "name": "games"
            },
            {
                "idx": 6,
                "output": "DP-1",
                "name": null
            },
            {
                "idx": 2,
                "output": "DP-1",
                "name": "media"
            },
            {
                "idx": 1,
                "output": "DP-1",
                "name": "code",
                "is_focused": true
            }
        ];
        const groups = groupWorkspaces(sample);
        if (groups.length !== 2)
            failures.push(`expected 2 groups, got ${groups.length}`);

        const dp1 = groups.filter(g => g.output === "DP-1")[0] || null;
        const dp2 = groups.filter(g => g.output === "DP-2")[0] || null;
        if (!dp1)
            failures.push("missing DP-1 group");

        if (!dp2)
            failures.push("missing DP-2 group");

        if (dp1) {
            const idxs = JSON.stringify(dp1.workspaces.map(w => w.idx));
            if (idxs !== "[1,2,3]")
                failures.push(`DP-1 idx order ${idxs} != [1,2,3]`);
        }
        if (dp2) {
            const idxs = JSON.stringify(dp2.workspaces.map(w => w.idx));
            if (idxs !== "[4,5]")
                failures.push(`DP-2 idx order ${idxs} != [4,5]`);
        }
        const total = groups.reduce((sum, g) => sum + g.workspaces.length, 0);
        if (total !== 5)
            failures.push(`expected 5 pills after empty filter, got ${total}`);

        try {
            const focused = focusedOutputNameFromWorkspaces(sample);
            if (focused !== "DP-1")
                failures.push(`focused output ${focused} != DP-1`);
        } catch (error) {
            failures.push(`focused output helper missing: ${error}`);
        }

        try {
            const shortcuts = parseWorkspaceShortcuts('binds {\n  Mod+1 { focus-workspace "code" }\n  Mod+4 { focus-workspace "browser" }\n  Mod+Shift+4 { move-column-to-workspace "browser" }\n}');
            if (shortcuts.code !== "1" || shortcuts.browser !== "4")
                failures.push(`workspace shortcuts ${JSON.stringify(shortcuts)} != code/browser 1/4`);
        } catch (error) {
            failures.push(`workspace shortcut parser missing: ${error}`);
        }

        return {
            "pass": failures.length === 0,
            "failures": failures
        };
    }

    function parsePickedColor(text) {
        const value = String(text || "");
        const match = value.match(/#[0-9a-fA-F]{6}/);
        lastPickedColor = match ? match[0].toLowerCase() : "";
        lastError = value.trim();
    }

    function windowById(id) {
        if (id === null || id === undefined)
            return null;

        for (let i = 0; i < windows.length; i++) {
            if (windows[i].id === id)
                return windows[i];
        }
        return null;
    }

    function windowActions(id) {
        return [
            {
                "id": "focus",
                "label": "focus",
                "route": "Niri.qml",
                "niriAction": "focus-window",
                "destructive": false,
                "keyboard": "Enter"
            },
            {
                "id": "toggle-fullscreen",
                "label": "fullscreen",
                "route": "Niri.qml",
                "niriAction": "fullscreen-window",
                "destructive": false,
                "keyboard": "F"
            },
            {
                "id": "move-workspace",
                "label": "move workspace",
                "route": "Niri.qml",
                "niriAction": "move-window-to-workspace",
                "destructive": false,
                "keyboard": "M"
            },
            {
                "id": "close-window",
                "label": "close",
                "route": "Niri.qml",
                "niriAction": "close-window",
                "destructive": true,
                "keyboard": "Delete"
            }
        ].map(action => {
            return Object.assign({}, action, {
                "windowId": id
            });
        });
    }

    function windowPanelContract() {
        return {
            "source": "Nuguland WindowInfo.qml Details.qml Buttons.qml plus Nuguland TaskViewContent.qml",
            "ownedBy": "PopoutHost.qml inline WindowPanel",
            "niriWindowCardPanel": true,
            "previewSlotCard": true,
            "detailsRows": true,
            "ruleHintRows": true,
            "windowActionRail": true,
            "workspaceMoveAffordance": true,
            "serviceActionsOnly": true,
            "route": "Niri.qml",
            "realNiriWindowState": true,
            "cardParts": ["preview-slot", "details", "rule-hints", "actions", "workspace-move"],
            "noCompositorOverviewClone": true
        };
    }

    function windowCardObject() {
        const win = focusedWindowRecord || ({});
        const appId = win.app_id || win.appId || "";
        const title = win.title || "";
        const workspace = win.workspace_id ?? win.workspaceId ?? null;
        const id = win.id ?? focusedWindow ?? null;
        const hints = [];
        if (appId.length > 0)
            hints.push(`match app-id=${JSON.stringify(appId)}`);

        if (title.length > 0)
            hints.push(`match title=${JSON.stringify(title)}`);

        if (workspace !== null)
            hints.push(`workspace id ${workspace}`);

        const actions = windowActions(id);
        return {
            "id": id,
            "appId": appId,
            "title": title,
            "workspace": workspace,
            "focused": focusedWindowRecord !== null,
            "ruleHints": hints,
            "previewSlot": {
                "source": "niri-toplevel",
                "available": focusedWindowRecord !== null,
                "live": focusedWindowRecord !== null,
                "title": title,
                "appId": appId,
                "role": "content.normal"
            },
            "actionRow": {
                "route": "Niri.qml",
                "keyboardFirst": true,
                "pointerParity": true,
                "actions": actions
            },
            "windowPanelContract": windowPanelContract(),
            "detailsRows": [
                {
                    "id": "app-id",
                    "label": "app-id",
                    "value": appId,
                    "role": "content.normal"
                },
                {
                    "id": "title",
                    "label": "title",
                    "value": title,
                    "role": "content.normal"
                },
                {
                    "id": "workspace",
                    "label": "workspace",
                    "value": workspace === null ? "unknown" : String(workspace),
                    "role": "ui.accent"
                },
                {
                    "id": "focused",
                    "label": "focused",
                    "value": focusedWindowRecord !== null ? "yes" : "no",
                    "role": focusedWindowRecord !== null ? "ui.accent" : "ui.minor"
                }
            ],
            "actions": actions,
            "lastWindowAction": lastWindowAction
        };
    }

    function windowRuleMetadata() {
        return JSON.stringify(windowCardObject());
    }

    function windowCardAction(kind) {
        const name = String(kind);
        const card = windowCardObject();
        const actionRows = card.actions || [];
        let selected = null;
        for (let i = 0; i < actionRows.length; i++)
            if (actionRows[i].id === name) {
                selected = actionRows[i];
            }
        lastWindowAction = {
            "kind": name,
            "route": "Niri.qml",
            "accepted": selected !== null,
            "niriAction": selected ? selected.niriAction : "",
            "windowId": card.id,
            "executed": false
        };
        return JSON.stringify(windowCardObject());
    }

    function stateJson() {
        return JSON.stringify({
            "available": available,
            "ready": ready,
            "workspaceCount": workspaces.length,
            "windowCount": windows.length,
            "windows": windows,
            "outputCount": Object.keys(outputs).length,
            "outputError": outputError,
            "outputs": outputs,
            "workspaces": workspaces,
            "workspaceShortcuts": workspaceShortcuts,
            "overviewState": overviewState,
            "focusedWindow": focusedWindow,
            "focusedOutputName": focusedOutputName,
            "focusedAppId": focusedAppId,
            "focusedTitle": focusedTitle,
            "lastError": lastError
        });
    }

    function applyEvent(line) {
        if (streamFailed || String(line).trim().length === 0)
            return;
        try {
            const patch = NiriState.reduce(root, JSON.parse(line));
            if (patch === null)
                return;
            if (Object.prototype.hasOwnProperty.call(patch, "screenshotPath")) {
                screenshotCaptured(patch.screenshotPath);
                return;
            }
            for (const key of Object.keys(patch))
                root[key] = patch[key];
            ready = windowsInitialized && workspacesInitialized && overviewState !== "unknown";
            if (ready)
                retryDelay = 250;
        } catch (error) {
            failStream(String(error));
        }
    }

    onReadyChanged: if (ready)
        refreshOutputs()

    Component.onCompleted: {
        refreshOutputs();
        refresh();
    }

    Connections {
        target: Quickshell

        function onScreensChanged() {
            Qt.callLater(root.refreshOutputs);
        }
    }

    // Niri has no OutputsChanged event. Qt screen notifications cover hotplug;
    // polling also catches compositor-only geometry changes. One query at a time.
    Timer {
        interval: 5000
        running: root.available
        repeat: true
        onTriggered: root.refreshOutputs()
    }

    Timer {
        id: reconnect

        onTriggered: root.refresh()
    }

    Timer {
        interval: 5000
        running: query.running && !root.ready && !root.streamFailed
        onTriggered: root.failStream("event stream initialization timed out")
    }

    Timer {
        interval: 3000
        running: outputQuery.running
        onTriggered: outputQuery.signal(9)
    }

    FileView {
        id: keybindFile

        path: root.keybindConfigPath
        preload: true
        printErrors: false
        onLoaded: root.reloadWorkspaceShortcuts()
        onLoadFailed: root.workspaceShortcuts = ({})
    }

    Process {
        id: outputQuery

        onRunningChanged: {
            if (!running) {
                if (!root.outputsFinished)
                    root.outputError = "outputs query failed to start";
                if (root.outputsPending)
                    Qt.callLater(root.refreshOutputs);
            }
        }

        stdout: StdioCollector {
            id: outputStdout

            waitForEnd: true
        }
    }

    Process {
        id: query

        running: false
        onRunningChanged: if (!running)
            root.streamDisconnected()

        stdout: SplitParser {
            onRead: data => {
                return root.applyEvent(data);
            }
        }

        stderr: StdioCollector {
            onStreamFinished: {
                if (this.text.trim().length > 0 && !root.streamFailed)
                    root.lastError = this.text.trim();
            }
        }
    }

    Connections {
        target: outputQuery

        function onExited(code, status) {
            root.outputsFinished = true;
            root.applyOutputs(outputStdout.text, code, status);
        }
    }

    Connections {
        target: query

        function onExited(code) {
            if (!root.streamFailed)
                root.lastError = `event stream exited ${code}`;
        }
    }

    Process {
        id: actionProc

        onRunningChanged: if (!running)
            root.dispatchNextAction()

        stderr: StdioCollector {
            onStreamFinished: root.lastError = this.text
        }
    }

    Process {
        id: colorProc

        stdout: StdioCollector {
            onStreamFinished: root.parsePickedColor(this.text)
        }

        stderr: StdioCollector {
            onStreamFinished: root.lastError = this.text
        }
    }
}