Files
att_menu/services/Attwm.qml
T
2026-07-26 21:16:55 +02:00

124 lines
4.3 KiB
QML

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;
}
}
}