initial commit
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
singleton AttWm 1.0 AttWm.qml
|
||||
@@ -0,0 +1,175 @@
|
||||
// A dwm-style bar for att_wm.
|
||||
//
|
||||
// Run with: quickshell -p /path/to/att_wm/quickshell
|
||||
//
|
||||
// Left click a tag to view it, right click to toggle it into the view,
|
||||
// middle click to move the focused window there. The layout symbol cycles
|
||||
// layouts on click, mirroring dwm's bar.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
ShellRoot {
|
||||
Variants {
|
||||
// One bar per monitor.
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: bar
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
// att_wm reads layer-shell exclusive zones and lays windows out in
|
||||
// what is left, so the bar never overlaps a window.
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: 26
|
||||
exclusiveZone: 26
|
||||
color: "#1a1a1a"
|
||||
|
||||
readonly property var output: AttWm.outputFor(modelData.name)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── Tags ────────────────────────────────────────────────
|
||||
Repeater {
|
||||
model: AttWm.tagCount
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
|
||||
readonly property bool active: AttWm.tagActive(bar.output, index)
|
||||
readonly property bool occupied: AttWm.tagOccupied(bar.output, index)
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: label.implicitWidth + 16
|
||||
color: active ? "#5294e2" : "transparent"
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: AttWm.tagNames[parent.index] ?? (parent.index + 1)
|
||||
color: parent.active ? "#1a1a1a" : (parent.occupied ? "#eeeeee" : "#666666")
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 13
|
||||
font.bold: parent.active
|
||||
}
|
||||
|
||||
// dwm draws a small square in the corner of a tag that
|
||||
// holds windows but is not currently shown.
|
||||
Rectangle {
|
||||
visible: parent.occupied && !parent.active
|
||||
x: 3
|
||||
y: 3
|
||||
width: 4
|
||||
height: 4
|
||||
color: "#5294e2"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: event => {
|
||||
const tag = parent.index + 1;
|
||||
if (event.button === Qt.LeftButton)
|
||||
AttWm.send(`view ${tag}`);
|
||||
else if (event.button === Qt.RightButton)
|
||||
AttWm.send(`toggle-view ${tag}`);
|
||||
else
|
||||
AttWm.send(`tag ${tag}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Layout symbol ───────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: layoutLabel.implicitWidth + 16
|
||||
color: "#242424"
|
||||
|
||||
Text {
|
||||
id: layoutLabel
|
||||
anchors.centerIn: parent
|
||||
text: bar.output ? bar.output.layout_symbol : "[]="
|
||||
color: "#bbbbbb"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: event => AttWm.send(
|
||||
event.button === Qt.LeftButton ? "cycle-layout next" : "cycle-layout prev")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Focused window title ────────────────────────────────
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
elide: Text.ElideRight
|
||||
text: AttWm.focusedTitle
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
// ── Tabs, for the tabbed layout ─────────────────────────
|
||||
//
|
||||
// att_wm draws its own solid-colour tab strip; this shows the
|
||||
// titles alongside it. Set `tabbar_height = 0` in config.zig to
|
||||
// rely on this instead.
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
spacing: 2
|
||||
visible: bar.output && bar.output.layout === "tabbed"
|
||||
|
||||
Repeater {
|
||||
model: bar.output
|
||||
? bar.output.windows.filter(w => w.visible && !w.floating)
|
||||
: []
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: Math.min(160, tabText.implicitWidth + 16)
|
||||
color: modelData.focused ? "#5294e2" : "#2c2c2c"
|
||||
|
||||
Text {
|
||||
id: tabText
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 12
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData.title || modelData.app_id
|
||||
color: parent.modelData.focused ? "#1a1a1a" : "#cccccc"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connection indicator ────────────────────────────────
|
||||
Text {
|
||||
Layout.rightMargin: 10
|
||||
visible: !AttWm.connected
|
||||
text: "att_wm ✕"
|
||||
color: "#e06c75"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user