116 lines
3.9 KiB
QML
116 lines
3.9 KiB
QML
// Singleton service wrapping att_wm's IPC socket.
|
|
//
|
|
// One connection does both jobs: it subscribes to state, and commands are
|
|
// written back down the same socket. att_wm keeps subscribers connected after a
|
|
// command precisely so a bar does not need a second connection or a process
|
|
// spawn per click.
|
|
|
|
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// Latest state from att_wm, parsed.
|
|
readonly property var outputs: state.outputs ?? []
|
|
readonly property int tagCount: state.tag_count ?? 9
|
|
readonly property var tagNames: state.tag_names ?? []
|
|
readonly property bool locked: state.locked ?? false
|
|
readonly property bool connected: link.item ? link.item.connected : false
|
|
|
|
// 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 "";
|
|
}
|
|
|
|
property var state: ({})
|
|
|
|
/// 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 att_wmctl, e.g. send("view 3").
|
|
function send(command) {
|
|
if (root.connected) link.item.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;
|
|
}
|
|
|
|
// A Socket that fails to connect cannot be revived: no state change is
|
|
// reported and re-asserting `connected` does not make it try again. So the
|
|
// socket lives in a Loader and a retry means building a new one. This is
|
|
// what lets the bar be started before att_wm, as `river/init` does.
|
|
Loader {
|
|
id: link
|
|
|
|
active: true
|
|
sourceComponent: Socket {
|
|
// att_wm names its socket after the Wayland display, so several
|
|
// river sessions can run side by side without colliding.
|
|
path: {
|
|
const explicit = Quickshell.env("ATT_WM_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
|
|
|
|
// att_wm replies with a full state object immediately, so there is
|
|
// no empty-bar flash 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("att_wm: bad state line:", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// att_wm may be restarted independently of the bar; keep trying.
|
|
Timer {
|
|
interval: 1000
|
|
repeat: true
|
|
running: !root.connected
|
|
onTriggered: {
|
|
// Drop what the dead connection last said, so a reconnect cannot
|
|
// briefly show state from before the restart.
|
|
root.state = ({});
|
|
link.active = false;
|
|
link.active = true;
|
|
}
|
|
}
|
|
}
|