124 lines
4.3 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|