add attwm support

This commit is contained in:
2026-07-26 13:48:27 +02:00
parent 9058dd7926
commit 202879eb0e
10 changed files with 404 additions and 44 deletions
+15 -5
View File
@@ -35,7 +35,7 @@ PanelWindow {
// depend on which row anything sits in, so this can't bind-loop.
readonly property real neededWidth:
(Theme.gap + 2) * 2 // outer margins
+ workspaces.implicitWidth
+ leftRow.implicitWidth
+ Theme.gap * 4 // breathing room between clusters
+ metricsRow.implicitWidth
+ Theme.gap
@@ -50,13 +50,23 @@ PanelWindow {
color: Theme.barColor
}
// row 1 left: workspaces
Workspaces {
id: workspaces
// row 1 left: workspaces, plus the layout symbol under attwm (hidden
// otherwise, and a hidden item takes no space in a RowLayout)
RowLayout {
id: leftRow
anchors.left: parent.left
anchors.leftMargin: Theme.gap + 2
y: (Theme.barHeight - height) / 2
screen: panel.modelData
spacing: Theme.gap
Workspaces {
screen: panel.modelData
Layout.alignment: Qt.AlignVCenter
}
LayoutSymbol {
screen: panel.modelData
Layout.alignment: Qt.AlignVCenter
}
}
// row 1 right: clock, tray
+42
View File
@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Layouts
import "../config"
import "../services"
// attwm's layout indicator: "[]=" master, "[M]" monocle, "|||" tabbed. Click
// cycles forward, right-click back -- dwm's bar binding.
//
// Layout is per output, so this shows the monitor the bar is on, not the
// focused one. Hidden entirely under any other window manager; the `&&` guards
// short-circuit before Attwm is touched, so the socket is never opened there.
MouseArea {
id: root
required property var screen
readonly property var output: Compositor.isAttwm
? Attwm.outputFor(screen ? screen.name : "")
: null
visible: Compositor.isAttwm && Attwm.connected && !!output
implicitWidth: pill.implicitWidth
implicitHeight: pill.implicitHeight
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: event => Attwm.send(event.button === Qt.RightButton
? "cycle-layout prev"
: "cycle-layout next")
Pill {
id: pill
anchors.fill: parent
Text {
text: root.output ? root.output.layout_symbol : ""
font.family: Theme.monoFont
font.pixelSize: Theme.fontSize
color: Theme.subtext
Layout.alignment: Qt.AlignVCenter
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import "../config"
// One workspace/tag chip. Every backend renders through this, so the left end
// of the bar looks the same whichever window manager is driving it.
//
// `dimmed` and `dot` are presentational rather than semantic on purpose: what
// counts as "empty" differs per backend (i3 only lists workspaces that exist,
// attwm always publishes all nine tags), so each call site decides and this
// stays a plain renderer.
Rectangle {
id: chip
property string label
property bool active: false // shown on this monitor
property bool focused: false // shown *and* holding input focus
property bool urgent: false
property bool dimmed: false // exists, but nothing on it
property bool dot: false // has windows but isn't shown (dwm's corner square)
signal clicked(int button)
implicitWidth: Math.max(implicitHeight, text.implicitWidth + 16)
implicitHeight: Theme.barHeight - Theme.gap * 2
width: implicitWidth
height: implicitHeight
radius: Theme.radius
color: urgent ? Theme.red
: focused ? Theme.blue
: active ? Theme.surface1
: Theme.surface0
Text {
id: text
anchors.centerIn: parent
text: chip.label
font.family: Theme.font
font.pixelSize: Theme.fontSize
font.bold: chip.focused || chip.urgent
color: (chip.focused || chip.urgent) ? Theme.mantle
: chip.dimmed ? Theme.overlay
: Theme.text
}
// dwm puts this in the tag's top-left corner, but the chip is only as wide
// as one digit -- a corner mark lands on the glyph. Underlining it says the
// same thing and suits the rounded chips.
Rectangle {
visible: chip.dot
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
width: 6
height: 2
radius: 1
color: Theme.blue
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: event => chip.clicked(event.button)
}
}
+62 -35
View File
@@ -4,59 +4,86 @@ import Quickshell
import Quickshell.I3
import Quickshell.Hyprland
import "../config"
import "../services"
// Workspace switcher for a single monitor. Works on both Sway/i3 and Hyprland:
// the compositor is detected once from its instance env var, and the matching
// Quickshell backend model is used. The two backends expose the same item shape
// (name / active / focused / urgent / monitor.name / activate()), so a single
// chip renderer covers both. The `?:` on `model` only evaluates the taken
// branch, so the unused backend singleton is never instantiated (no stray IPC).
// Workspace switcher for a single monitor. Three backends, one chip renderer
// (WorkspaceChip):
//
// * Sway/i3 and Hyprland expose the same item shape (name / active / focused
// / urgent / monitor.name / activate()), so both feed the same Repeater.
// * attwm is a different model entirely -- nine fixed tags addressed by
// bitmask, several of which can be viewed at once -- so it gets its own
// Repeater over an index range.
//
// Exactly one of the two Repeaters is ever non-empty. The `?:` and `&&` guards
// only evaluate the taken branch, which is what keeps the unused backend
// singletons from being instantiated (no stray IPC) -- see Compositor.
Row {
id: root
required property var screen // ShellScreen this bar lives on
spacing: Theme.gap
// Which compositor are we under? Hyprland always exports this signature.
readonly property bool isHyprland: !!Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
// Name of the output this bar is on, used to filter workspaces.
readonly property string monitorName: screen ? screen.name : ""
Repeater {
model: root.isHyprland ? Hyprland.workspaces : I3.workspaces
// attwm keeps tags, layout and geometry per output, so the tag row on this
// bar reflects this monitor rather than whichever one has focus.
readonly property var attwmOutput: Compositor.isAttwm
? Attwm.outputFor(monitorName)
: null
Rectangle {
id: chip
// ── Sway/i3 and Hyprland ────────────────────────────────────────────────
Repeater {
model: Compositor.isAttwm ? 0
: Compositor.isHyprland ? Hyprland.workspaces
: I3.workspaces
WorkspaceChip {
required property var modelData
// only show workspaces belonging to this monitor. Both I3Workspace
// and HyprlandWorkspace expose .monitor as an object with a .name.
visible: modelData.monitor && modelData.monitor.name === root.monitorName
width: visible ? Math.max(height, label.implicitWidth + 16) : 0
height: Theme.barHeight - Theme.gap * 2
radius: Theme.radius
readonly property bool isFocused: modelData.focused
readonly property bool isUrgent: modelData.urgent
label: modelData.name
active: modelData.active
focused: modelData.focused
urgent: modelData.urgent
color: isUrgent ? Theme.red
: isFocused ? Theme.blue
: modelData.active ? Theme.surface1
: Theme.surface0
onClicked: modelData.activate()
}
}
Text {
id: label
anchors.centerIn: parent
text: chip.modelData.name
font.family: Theme.font
font.pixelSize: Theme.fontSize
font.bold: chip.isFocused || chip.isUrgent
color: (chip.isFocused || chip.isUrgent) ? Theme.mantle : Theme.text
}
// ── attwm ───────────────────────────────────────────────────────────────
Repeater {
// 0 until the socket has produced a state object, so nothing is drawn
// if we guessed the window manager wrong.
model: Compositor.isAttwm ? Attwm.tagCount : 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: chip.modelData.activate()
WorkspaceChip {
id: tag
required property int index
readonly property bool viewing: Attwm.tagActive(root.attwmOutput, index)
readonly property bool occupied: Attwm.tagOccupied(root.attwmOutput, index)
label: Attwm.tagNames[index] ?? (index + 1)
active: viewing
// Tags are per output and several outputs can view a tag at once;
// only the focused output's get the focused colour.
focused: viewing && !!root.attwmOutput && root.attwmOutput.focused
dimmed: !viewing && !occupied
dot: occupied && !viewing
// dwm's mouse bindings: view / toggle into view / send window there.
onClicked: button => {
const n = index + 1;
if (button === Qt.RightButton)
Attwm.send(`toggle-view ${n}`);
else if (button === Qt.MiddleButton)
Attwm.send(`tag ${n}`);
else
Attwm.send(`view ${n}`);
}
}
}