Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Clipboard.qml

Raw
import QtQuick
import Quickshell
import Quickshell.Io
import "clip.js" as Clip

Item {
    id: root

    property SurfaceManager surfaces
    property Niri niriService
    property string pendingPasteId: ""
    property var pasteWindowId: null
    property int pasteAttempts: 0
    property string focusReply: ""
    property bool privateMode: false
    property bool vaultCopyActive: false
    property bool vaultPauseAcknowledged: false
    property var rows: []
    signal vaultCapturePaused
    property var lastAction: ({})
    property bool persistenceReady: false
    readonly property int maxRows: 40
    readonly property string historyPath: Quickshell.statePath("clipboard-history.json")

    // --- linear lookup over the raw persisted rows (id, title, body, pinned, createdAt) ---
    function rawRowById(id) {
        const needle = String(id);
        for (let i = 0; i < rows.length; i++)
            if (rows[i].id === needle) {
                return rows[i];
            }
        return null;
    }

    function captureClipboard(text) {
        const body = String(text || "");
        if (privateMode || !Clip.isReadableText(body))
            return;

        const id = `clip-${Clip.hashText(body)}`;
        const next = [];
        let existingPin = false;
        for (let i = 0; i < rows.length; i++) {
            if (rows[i].id === id) {
                existingPin = !!rows[i].pinned;
                continue;
            }
            next.push(rows[i]);
        }
        next.unshift({
            "id": id,
            "title": Clip.rowTitle(body),
            "body": body,
            "pinned": existingPin,
            "createdAt": Date.now()
        });
        rows = next.slice(0, maxRows);
        saveHistory();
    }

    // --- trimmed serialization: the view binds the cloned rows directly ---
    function stateObject(query) {
        const now = Date.now();
        const viewRows = Clip.filterRows(rows, query).map(row => {
            return Clip.cloneRow(row, now);
        });
        return {
            "privateMode": privateMode,
            "privateRole": privateMode ? "warning" : "ui.accent",
            "rows": viewRows,
            "lastAction": lastAction
        };
    }

    function stateJson() {
        return JSON.stringify(stateObject(""));
    }

    function search(query) {
        return JSON.stringify(stateObject(query));
    }

    function launcherRows(query) {
        const now = Date.now();
        const result = Clip.filterRows(rows, query).map(row => {
            return Clip.cloneRow(row, now);
        });
        for (let i = 0; i < result.length; i++) {
            result[i].subtitle = privateMode ? "private clipboard" : "clipboard";
            result[i].keyboard = true;
        }
        return JSON.stringify(result);
    }

    function mutateRow(id, callback) {
        const next = [];
        for (let i = 0; i < rows.length; i++) {
            const row = Object.assign({}, rows[i]);
            if (row.id === String(id))
                callback(row);

            next.push(row);
        }
        rows = next;
        saveHistory();
    }

    function togglePin(id) {
        mutateRow(id, row => {
            return row.pinned = !row.pinned;
        });
        lastAction = {
            "kind": "pin",
            "id": String(id)
        };
        return stateJson();
    }

    function copy(id) {
        const row = rawRowById(id);
        if (row)
            Quickshell.execDetached(["wl-copy", String(row.body || "")]);

        lastAction = {
            "kind": "copy",
            "id": String(id),
            "accepted": !!row
        };
        return stateJson();
    }

    function paste(id) {
        if (pendingPasteId.length > 0)
            return stateJson();
        const target = surfaces && surfaces.popoutOpen ? surfaces.returnWindowId : (niriService ? niriService.focusedWindow : null);
        const accepted = !!rawRowById(id) && target !== null && target !== undefined;
        lastAction = {
            "kind": "paste",
            "id": String(id),
            "accepted": accepted,
            "pending": accepted,
            "error": accepted ? "" : "no paste destination",
            "route": "clipboard-target-typing",
            "pasteInputRoute": "wtype"
        };
        if (!accepted)
            return stateJson();
        pendingPasteId = String(id);
        pasteWindowId = target;
        pasteAttempts = 0;
        if (surfaces)
            surfaces.closePopout("clipboard-paste");
        // Wait for observable compositor focus, not a guessed typing delay.
        pasteFocusTimer.start();
        return stateJson();
    }

    function finishPaste(error) {
        lastAction = Object.assign({}, lastAction, {
            "pending": false,
            "error": error
        });
        pendingPasteId = "";
        pasteWindowId = null;
        focusReply = "";
        pasteFocusTimer.stop();
    }

    function checkPasteFocus(exitCode) {
        if (pendingPasteId.length === 0)
            return;
        if (surfaces && surfaces.popoutOpen) {
            finishPaste("paste cancelled: another panel opened");
            return;
        }
        let focused = null;
        try {
            focused = JSON.parse(focusReply);
        } catch (_) {}
        if (exitCode === 0 && focused && focused.id === pasteWindowId) {
            const row = rawRowById(pendingPasteId);
            if (row)
                Quickshell.execDetached(["/usr/bin/wtype", "--", String(row.body || "")]);
            finishPaste(row ? "" : "clipboard item no longer available");
        } else if (++pasteAttempts >= 40 || (focused && focused.id !== pasteWindowId)) {
            finishPaste("paste destination lost focus");
        } else {
            pasteFocusTimer.restart();
        }
    }

    function deleteRow(id) {
        const target = String(id);
        const next = [];
        for (let i = 0; i < rows.length; i++) {
            if (rows[i].id !== target)
                next.push(rows[i]);
        }
        rows = next;
        saveHistory();
        lastAction = {
            "kind": "delete",
            "id": target
        };
        return stateJson();
    }

    function loadHistoryText(text) {
        try {
            const parsed = JSON.parse(String(text || "{}"));
            const loadedRows = Array.isArray(parsed.rows) ? parsed.rows : [];
            rows = loadedRows.map(row => {
                return Clip.persistedRow(row);
            }).filter(row => {
                return row.id.length > 0 && Clip.isReadableText(row.body);
            }).slice(0, maxRows);
        } catch (error) {
            rows = [];
        }
        persistenceReady = true;
        saveHistory();
    }

    function saveHistory() {
        if (!persistenceReady)
            return;

        const payload = {
            "version": 1,
            "rows": rows.map(row => {
                return Clip.persistedRow(row);
            }).slice(0, maxRows)
        };
        clipboardHistoryFile.setText(JSON.stringify(payload, null, 2));
    }

    function setPrivateMode(enabled) {
        privateMode = enabled;
        lastAction = {
            "kind": "private",
            "enabled": enabled
        };
        return stateJson();
    }

    function pauseForVaultCopy(timeoutSeconds) {
        vaultPauseAcknowledged = false;
        vaultCopyActive = true;
        vaultCopyFailsafe.interval = (Math.max(1, Number(timeoutSeconds || 30)) + 2) * 1000;
        vaultCopyFailsafe.restart();
        if (!clipboardWatcher.running) {
            vaultPauseAcknowledged = true;
            vaultCapturePaused();
        }
    }

    function resumeAfterVaultCopy() {
        vaultCopyFailsafe.stop();
        vaultCopyActive = false;
        vaultPauseAcknowledged = false;
    }

    // --- exercises clip.js detect/sort/filter/clone through the service ---
    function selfTest() {
        const cases = [];
        function ok(name, cond) {
            cases.push({
                "name": name,
                "pass": !!cond
            });
        }
        try {
            ok("detect-url", Clip.detectKind("https://example.com/x").kind === "url");
            ok("detect-color", Clip.detectKind("#ff0000").kind === "color");
            ok("detect-image", Clip.detectKind("/tmp/shot.png").kind === "image");
            ok("detect-path", Clip.detectKind("/etc/hosts").kind === "path");
            ok("detect-text", Clip.detectKind("just some words").kind === "text");
            const sorted = Clip.sortRows([
                {
                    "id": "a",
                    "title": "a",
                    "body": "a",
                    "pinned": false,
                    "createdAt": 100
                },
                {
                    "id": "b",
                    "title": "b",
                    "body": "b",
                    "pinned": false,
                    "createdAt": 200
                },
                {
                    "id": "c",
                    "title": "c",
                    "body": "c",
                    "pinned": true,
                    "createdAt": 50
                }
            ]);
            ok("sort-pinned-first", sorted[0].id === "c");
            ok("sort-recency-desc", sorted[1].id === "b" && sorted[2].id === "a");
            const filtered = Clip.filterRows([
                {
                    "id": "x",
                    "title": "hello",
                    "body": "world",
                    "pinned": false,
                    "createdAt": 1
                },
                {
                    "id": "y",
                    "title": "foo",
                    "body": "bar",
                    "pinned": false,
                    "createdAt": 2
                }
            ], "foo");
            ok("filter-match", filtered.length === 1 && filtered[0].id === "y");
            const imageRow = Clip.cloneRow({
                "id": "i",
                "body": "/tmp/p.png",
                "pinned": false,
                "createdAt": Date.now()
            });
            ok("clone-image", imageRow.isImage === true && imageRow.title === "Image" && imageRow.imagePath === "/tmp/p.png");
            const colorRow = Clip.cloneRow({
                "id": "c2",
                "body": "#abc",
                "pinned": false,
                "createdAt": Date.now()
            });
            ok("clone-swatch", colorRow.kind === "color" && colorRow.swatch === "#aabbcc");
        } catch (error) {
            ok("threw:" + error, false);
        }
        const fail = cases.filter(c => {
            return !c.pass;
        }).length;
        return JSON.stringify({
            "pass": cases.length - fail,
            "fail": fail,
            "cases": cases
        });
    }

    FileView {
        id: clipboardHistoryFile

        path: root.historyPath
        blockWrites: true
        atomicWrites: true
        printErrors: false
        onLoaded: root.loadHistoryText(clipboardHistoryFile.text())
        onLoadFailed: {
            root.persistenceReady = true;
            root.saveHistory();
        }
    }

    Timer {
        id: pasteFocusTimer
        interval: 25
        onTriggered: {
            root.focusReply = "";
            pasteFocusQuery.running = true;
        }
    }

    Process {
        id: pasteFocusQuery
        command: ["/usr/bin/niri", "msg", "--json", "focused-window"]
        stdout: StdioCollector {
            onStreamFinished: root.focusReply = text
        }
    }

    Connections {
        target: pasteFocusQuery
        function onExited(exitCode) {
            root.checkPasteFocus(exitCode);
        }
    }

    Timer {
        id: vaultCopyFailsafe

        onTriggered: root.resumeAfterVaultCopy()
    }

    Process {
        id: clipboardWatcher

        command: ["wl-paste", "--type", "text/plain", "--watch", "sh", "-c", "cat; printf '\\036'"]
        running: !root.vaultCopyActive
        onRunningChanged: {
            if (root.vaultCopyActive && !running && !root.vaultPauseAcknowledged) {
                root.vaultPauseAcknowledged = true;
                root.vaultCapturePaused();
            }
        }

        stdout: SplitParser {
            splitMarker: "\u001e"
            onRead: data => {
                return root.captureClipboard(data);
            }
        }
    }
}