// Singleton service wrapping attwm's IPC socket. // // Adapted from the reference client that ships with attwm (quickshell/Attwm.qml // in that tree); keep the two in step when the state schema changes. // // One connection does both jobs: it subscribes to state, and commands are // written back down the same socket. attwm keeps subscribers connected after a // command precisely so a bar does not need a second connection or a process // spawn per click. // // Only instantiate this under attwm -- see Compositor.isAttwm. Merely reading a // property here opens the socket and starts the reconnect timer. pragma Singleton import QtQuick import Quickshell import Quickshell.Io Singleton { id: root // Latest state from attwm, parsed. Left untouched when the connection // drops, so a restart of attwm freezes the bar on the last known state // rather than blanking it for the second it takes to reconnect. property var state: ({}) readonly property var outputs: state.outputs ?? [] readonly property var tagNames: state.tag_names ?? [] readonly property bool locked: state.locked ?? false readonly property bool connected: socket.connected // 0 until the first state object lands: the tag row is driven by this, and // a default of 9 would paint a row of dead tags before (or without) attwm. readonly property int tagCount: state.tag_count ?? 0 // The output the user is currently working on. readonly property var focusedOutput: { for (const o of outputs) if (o.focused) return o; return outputs.length > 0 ? outputs[0] : null; } readonly property string focusedTitle: { if (!focusedOutput) return ""; for (const w of focusedOutput.windows) if (w.focused) return w.title; return ""; } /// Look up an output by name, so a multi-monitor bar can show per-screen /// state rather than the focused output's. function outputFor(name) { for (const o of outputs) if (o.name === name) return o; return null; } /// Send a command. Same grammar as attwmctl, e.g. send("view 3"). function send(command) { if (socket.connected) socket.write(command + "\n"); } // A tag is "occupied" if some window carries it, mirroring dwm's bar. function tagOccupied(output, index) { return output ? (output.occupied & (1 << index)) !== 0 : false; } function tagActive(output, index) { return output ? (output.tags & (1 << index)) !== 0 : false; } Socket { id: 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}/attwm-${display}.sock`; } connected: true onConnectionStateChanged: { if (connected) { // attwm replies with a full state object immediately, so there // is no empty-bar flash on connect. socket.write("subscribe\n"); } else { reconnect.running = true; } } 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); } } } } // attwm may be restarted independently of the bar; keep trying. Timer { id: reconnect interval: 1000 repeat: true running: false onTriggered: { if (socket.connected) { running = false; } else { socket.connected = true; } } } }