pragma Singleton import QtQuick import Quickshell import Quickshell.Io // attwm's IPC socket: the window list, and the commands that act on a window. // // river 0.4 ships no window management policy of its own — an external client // speaking river-window-management-v1 owns it, which here is attwm. That // protocol has no `activate_requested` event and focus is set solely by the // window manager, so the `activate` request wlr-foreign-toplevel-management // sends on a task-list click reaches nobody at all and focus never moves. This // socket is the way in. See modules/WindowList.qml. // // One connection does both jobs: it subscribes to state and commands go back // down the same socket, so a click costs no process spawn. Singleton { id: root // True once attwm has answered. Everything here is inert otherwise, which // is what keeps the panel working unchanged on other compositors. readonly property bool available: link.item ? link.item.connected : false // Only worth trying under river — anywhere else the socket will never // exist and the retry below would spend the session failing to connect. readonly property bool enabled: !!Quickshell.env("ATTWM_SOCKET") || (Quickshell.env("XDG_CURRENT_DESKTOP") || "").toLowerCase().includes("river") // Latest state object from attwm, parsed. property var state: ({}) readonly property var outputs: root.state.outputs ?? [] readonly property var allWindows: { const all = []; for (const out of root.outputs) all.push(...out.windows); return all; } function outputFor(name: string): var { for (const out of root.outputs) if (out.name === name) return out; return null; } // Both take a window id — the `id` field of a window in the state above, // which is river's own identifier for it. function focusWindow(id: string): void { root.send(`focus-window ${id}`); } function closeWindow(id: string): void { root.send(`close-window ${id}`); } function send(command: string): void { if (root.available) link.item.write(`${command}\n`); } // A Socket that fails to connect stays dead: it reports no state change and // re-asserting `connected` does not make it try again. Reconnecting // therefore means building a new one, so the socket lives in a Loader that // can be thrown away. The panel and attwm start independently and either // may restart, so giving up after the first attempt is not an option. Loader { id: link active: root.enabled sourceComponent: Socket { // attwm names its socket after the Wayland display, so several // river sessions can run side by side without colliding. path: { const explicit = Quickshell.env("ATTWM_SOCKET"); if (explicit) return explicit; const dir = Quickshell.env("XDG_RUNTIME_DIR"); const display = Quickshell.env("WAYLAND_DISPLAY") || "wayland-0"; return `${dir}/att_wm-${display}.sock`; } connected: true // attwm replies with a full state object immediately, so the window // list does not flash empty on connect. onConnectionStateChanged: if (connected) write("subscribe\n") parser: SplitParser { onRead: line => { // Command acknowledgements ("ok" / "err ...") share the // stream with state objects; only the latter are JSON. if (!line.startsWith("{")) return; try { root.state = JSON.parse(line); } catch (e) { console.warn("attwm: bad state line:", e); } } } } } Timer { interval: 1000 repeat: true running: root.enabled && !root.available onTriggered: { // Drop whatever the dead connection last said, so a reconnect // cannot briefly show a window list from before the restart. root.state = ({}); link.active = false; link.active = true; } } }