add attwm support
This commit is contained in:
@@ -104,6 +104,30 @@ the QML side parses those lines into `tempPath`/`batPath`, which then feed
|
|||||||
to pick up layer-surface fixes ahead of release.
|
to pick up layer-surface fixes ahead of release.
|
||||||
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
|
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
|
||||||
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
|
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
|
||||||
|
- **Window manager backends are picked by `config/Compositor.qml`**, a singleton
|
||||||
|
that reads *only* environment variables — Hyprland's instance signature,
|
||||||
|
`$SWAYSOCK`/`$I3SOCK`, else attwm as the fallback (it announces nothing of its
|
||||||
|
own, being a river client rather than a compositor). **Never let the detector
|
||||||
|
do IPC**, and **never read a backend singleton outside its guard**: touching
|
||||||
|
`Attwm.x` / `I3.x` / `Hyprland.x` instantiates it, and an idle backend sits
|
||||||
|
there retrying a socket that will never answer. Every use is behind
|
||||||
|
`Compositor.isAttwm && …` or a `?:` whose untaken branch is never evaluated —
|
||||||
|
that short-circuit *is* the mechanism. It's verifiable: under attwm the log
|
||||||
|
has no `$I3SOCK is unset` line, under Sway it does.
|
||||||
|
- **attwm** (`services/Attwm.qml`, adapted from the reference client in that
|
||||||
|
repo — keep the two in step) is one `Socket` doing both directions: it
|
||||||
|
`subscribe`s for a JSON state object per change and writes `attwmctl`-grammar
|
||||||
|
commands back down the same connection, so a click costs no process spawn.
|
||||||
|
Its model is dwm's, not i3's — nine fixed tags addressed by *bitmask*
|
||||||
|
(`tags`/`occupied`), several viewable at once, per output, and no urgency
|
||||||
|
(the protocol has no attention-request event). `tagCount` deliberately
|
||||||
|
defaults to 0, not 9, so a wrong detection guess renders nothing instead of a
|
||||||
|
row of dead tags. `state` is left stale on disconnect so an attwm restart
|
||||||
|
freezes the bar rather than blanking it.
|
||||||
|
- Every backend renders through **`widgets/WorkspaceChip.qml`**, whose `dimmed`
|
||||||
|
and `dot` are presentational rather than semantic — "empty" means different
|
||||||
|
things per backend (i3 only lists workspaces that exist; attwm always
|
||||||
|
publishes all nine tags), so the call site decides.
|
||||||
- **Making a module interactive (popups/drawers):** wrap the module's `Pill` in
|
- **Making a module interactive (popups/drawers):** wrap the module's `Pill` in
|
||||||
an `Item` (forward `implicitWidth`/`implicitHeight` from the pill so the bar's
|
an `Item` (forward `implicitWidth`/`implicitHeight` from the pill so the bar's
|
||||||
RowLayout still sizes it), add a `MouseArea`, and open a `Quickshell.PopupWindow`;
|
RowLayout still sizes it), add a `MouseArea`, and open a `Quickshell.PopupWindow`;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# quickshell-bar
|
# quickshell-bar
|
||||||
|
|
||||||
A Wayland status bar for [Sway](https://swaywm.org/), built with
|
A Wayland status bar for [Sway](https://swaywm.org/), i3,
|
||||||
|
[Hyprland](https://hypr.land/) and attwm (a dwm-like window manager for
|
||||||
|
[river](https://codeberg.org/river/river)), built with
|
||||||
[Quickshell](https://quickshell.outfoxxed.me/) and packaged with Nix flakes.
|
[Quickshell](https://quickshell.outfoxxed.me/) and packaged with Nix flakes.
|
||||||
|
|
||||||

|

|
||||||
@@ -11,7 +13,8 @@ Left → right:
|
|||||||
|
|
||||||
| Module | Source |
|
| Module | Source |
|
||||||
|-------------|------------------------------------------|
|
|-------------|------------------------------------------|
|
||||||
| Workspaces | Sway/i3 IPC (`Quickshell.I3`) |
|
| Workspaces | Sway/i3 IPC (`Quickshell.I3`), Hyprland IPC, or attwm tags |
|
||||||
|
| Layout | attwm only — layout symbol, click to cycle |
|
||||||
| Clock | local time (center) |
|
| Clock | local time (center) |
|
||||||
| CPU | `/proc/stat` (per-core load bars + overall %) |
|
| CPU | `/proc/stat` (per-core load bars + overall %) |
|
||||||
| CPU temp | `coretemp`/`k10temp` hwmon, else thermal |
|
| CPU temp | `coretemp`/`k10temp` hwmon, else thermal |
|
||||||
@@ -33,7 +36,12 @@ use native Quickshell services.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Sway (or i3) — the workspace module talks to `$SWAYSOCK`/`$I3SOCK`.
|
- A supported window manager, detected from the environment at startup:
|
||||||
|
Sway/i3 (`$SWAYSOCK`/`$I3SOCK`), Hyprland
|
||||||
|
(`$HYPRLAND_INSTANCE_SIGNATURE`), or attwm — assumed when neither of the
|
||||||
|
other two announces itself, and talking to
|
||||||
|
`$XDG_RUNTIME_DIR/attwm-$WAYLAND_DISPLAY.sock` (override with
|
||||||
|
`$ATTWM_SOCKET`).
|
||||||
- A running PipeWire session for the volume module.
|
- A running PipeWire session for the volume module.
|
||||||
- Nix with flakes enabled (`experimental-features = nix-command flakes`).
|
- Nix with flakes enabled (`experimental-features = nix-command flakes`).
|
||||||
|
|
||||||
@@ -74,6 +82,23 @@ Or, if you install the package (e.g. into your system/Home-Manager profile),
|
|||||||
just `exec quickshell-bar`. The bar anchors to the top edge and reserves an
|
just `exec quickshell-bar`. The bar anchors to the top edge and reserves an
|
||||||
exclusive zone, so windows tile beneath it automatically.
|
exclusive zone, so windows tile beneath it automatically.
|
||||||
|
|
||||||
|
## Use it from attwm
|
||||||
|
|
||||||
|
attwm is started by river, so launch the bar from river's init alongside it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
# ~/.config/river/init
|
||||||
|
quickshell-bar &
|
||||||
|
exec attwm
|
||||||
|
```
|
||||||
|
|
||||||
|
Tags replace workspaces: all nine are always shown, dim when empty, underlined
|
||||||
|
when they hold windows you aren't looking at. Left click views a tag, right
|
||||||
|
click toggles it into the view, middle click sends the focused window there.
|
||||||
|
The layout symbol sits to their right — click to cycle forward, right click
|
||||||
|
back. A bar can only map at all because attwm binds `river_layer_shell_v1`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Everything is plain QML — edit and the bar hot-reloads.
|
Everything is plain QML — edit and the bar hot-reloads.
|
||||||
@@ -91,13 +116,16 @@ shell.qml entry point — one Bar per monitor
|
|||||||
config/
|
config/
|
||||||
Theme.qml colours, sizes, fonts (singleton)
|
Theme.qml colours, sizes, fonts (singleton)
|
||||||
Icons.qml Nerd Font glyphs (singleton)
|
Icons.qml Nerd Font glyphs (singleton)
|
||||||
|
Compositor.qml which window manager are we under? env only (singleton)
|
||||||
services/
|
services/
|
||||||
SysStats.qml reads /proc & /sys via FileView into reactive props (singleton)
|
SysStats.qml reads /proc & /sys via FileView into reactive props (singleton)
|
||||||
|
Attwm.qml attwm IPC socket: state in, commands out (singleton)
|
||||||
discover.sh one-shot hwmon/battery path discovery at startup
|
discover.sh one-shot hwmon/battery path discovery at startup
|
||||||
widgets/
|
widgets/
|
||||||
Bar.qml the PanelWindow + layout
|
Bar.qml the PanelWindow + layout
|
||||||
Pill.qml rounded container used by every module
|
Pill.qml rounded container used by every module
|
||||||
MetricPill.qml icon + value helper
|
MetricPill.qml icon + value helper
|
||||||
Workspaces.qml Clock.qml CpuGraph.qml CpuTemp.qml
|
WorkspaceChip.qml one workspace/tag chip, shared by every backend
|
||||||
|
Workspaces.qml LayoutSymbol.qml Clock.qml CpuGraph.qml CpuTemp.qml
|
||||||
Ram.qml Disk.qml Network.qml Volume.qml Tray.qml Battery.qml
|
Ram.qml Disk.qml Network.qml Volume.qml Tray.qml Battery.qml
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Which window manager is driving this session? Detected once, from the
|
||||||
|
// environment only.
|
||||||
|
//
|
||||||
|
// This deliberately does no IPC of its own. Widgets branch on these flags to
|
||||||
|
// pick a backend, and reading a flag must never be able to instantiate the
|
||||||
|
// service singleton for a window manager that isn't running -- an idle backend
|
||||||
|
// would sit there retrying a socket that will never answer. Anything that
|
||||||
|
// wants a backend gates on the matching flag first (`Compositor.isAttwm &&
|
||||||
|
// Attwm.x`), relying on `&&` / `?:` to leave the other branch untouched.
|
||||||
|
Singleton {
|
||||||
|
// Hyprland always exports this signature.
|
||||||
|
readonly property bool _hyprSig: !!Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
|
|
||||||
|
// Sway and i3 both point clients at their IPC socket.
|
||||||
|
readonly property bool _i3Sock: !!Quickshell.env("SWAYSOCK")
|
||||||
|
|| !!Quickshell.env("I3SOCK")
|
||||||
|
|
||||||
|
// attwm exports no signature of its own: it is a window management client
|
||||||
|
// of river rather than a compositor, so there is nothing session-wide to
|
||||||
|
// look for. Treat it as the fallback -- an explicit socket override wins
|
||||||
|
// outright, otherwise assume attwm once the two that do announce
|
||||||
|
// themselves are ruled out. Guessing wrong is cheap: everything attwm
|
||||||
|
// renders also gates on `Attwm.connected`, so a session with no window
|
||||||
|
// manager we recognise shows nothing rather than a row of dead tags.
|
||||||
|
readonly property bool isAttwm: !!Quickshell.env("ATTWM_SOCKET")
|
||||||
|
|| (!_hyprSig && !_i3Sock)
|
||||||
|
|
||||||
|
readonly property bool isHyprland: !isAttwm && _hyprSig
|
||||||
|
readonly property bool isI3: !isAttwm && !isHyprland && _i3Sock
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
singleton Theme 1.0 Theme.qml
|
singleton Theme 1.0 Theme.qml
|
||||||
singleton Icons 1.0 Icons.qml
|
singleton Icons 1.0 Icons.qml
|
||||||
|
singleton Compositor 1.0 Compositor.qml
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Singleton service wrapping attwm's IPC socket.
|
||||||
|
//
|
||||||
|
// Adapted from the reference client that ships with attwm (quickshell/Attwm.qml
|
||||||
|
// in that tree); keep the two in step when the state schema changes.
|
||||||
|
//
|
||||||
|
// One connection does both jobs: it subscribes to state, and commands are
|
||||||
|
// written back down the same socket. attwm keeps subscribers connected after a
|
||||||
|
// command precisely so a bar does not need a second connection or a process
|
||||||
|
// spawn per click.
|
||||||
|
//
|
||||||
|
// Only instantiate this under attwm -- see Compositor.isAttwm. Merely reading a
|
||||||
|
// property here opens the socket and starts the reconnect timer.
|
||||||
|
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Latest state from attwm, parsed. Left untouched when the connection
|
||||||
|
// drops, so a restart of attwm freezes the bar on the last known state
|
||||||
|
// rather than blanking it for the second it takes to reconnect.
|
||||||
|
property var state: ({})
|
||||||
|
|
||||||
|
readonly property var outputs: state.outputs ?? []
|
||||||
|
readonly property var tagNames: state.tag_names ?? []
|
||||||
|
readonly property bool locked: state.locked ?? false
|
||||||
|
readonly property bool connected: socket.connected
|
||||||
|
|
||||||
|
// 0 until the first state object lands: the tag row is driven by this, and
|
||||||
|
// a default of 9 would paint a row of dead tags before (or without) attwm.
|
||||||
|
readonly property int tagCount: state.tag_count ?? 0
|
||||||
|
|
||||||
|
// 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 "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 attwmctl, e.g. send("view 3").
|
||||||
|
function send(command) {
|
||||||
|
if (socket.connected) socket.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket {
|
||||||
|
id: 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}/attwm-${display}.sock`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connected: true
|
||||||
|
|
||||||
|
onConnectionStateChanged: {
|
||||||
|
if (connected) {
|
||||||
|
// attwm replies with a full state object immediately, so there
|
||||||
|
// is no empty-bar flash on connect.
|
||||||
|
socket.write("subscribe\n");
|
||||||
|
} else {
|
||||||
|
reconnect.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attwm may be restarted independently of the bar; keep trying.
|
||||||
|
Timer {
|
||||||
|
id: reconnect
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: false
|
||||||
|
onTriggered: {
|
||||||
|
if (socket.connected) {
|
||||||
|
running = false;
|
||||||
|
} else {
|
||||||
|
socket.connected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
singleton SysStats 1.0 SysStats.qml
|
singleton SysStats 1.0 SysStats.qml
|
||||||
|
singleton Attwm 1.0 Attwm.qml
|
||||||
|
|||||||
+15
-5
@@ -35,7 +35,7 @@ PanelWindow {
|
|||||||
// depend on which row anything sits in, so this can't bind-loop.
|
// depend on which row anything sits in, so this can't bind-loop.
|
||||||
readonly property real neededWidth:
|
readonly property real neededWidth:
|
||||||
(Theme.gap + 2) * 2 // outer margins
|
(Theme.gap + 2) * 2 // outer margins
|
||||||
+ workspaces.implicitWidth
|
+ leftRow.implicitWidth
|
||||||
+ Theme.gap * 4 // breathing room between clusters
|
+ Theme.gap * 4 // breathing room between clusters
|
||||||
+ metricsRow.implicitWidth
|
+ metricsRow.implicitWidth
|
||||||
+ Theme.gap
|
+ Theme.gap
|
||||||
@@ -50,13 +50,23 @@ PanelWindow {
|
|||||||
color: Theme.barColor
|
color: Theme.barColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// row 1 left: workspaces
|
// row 1 left: workspaces, plus the layout symbol under attwm (hidden
|
||||||
Workspaces {
|
// otherwise, and a hidden item takes no space in a RowLayout)
|
||||||
id: workspaces
|
RowLayout {
|
||||||
|
id: leftRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Theme.gap + 2
|
anchors.leftMargin: Theme.gap + 2
|
||||||
y: (Theme.barHeight - height) / 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
|
// row 1 right: clock, tray
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -4,59 +4,86 @@ import Quickshell
|
|||||||
import Quickshell.I3
|
import Quickshell.I3
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import "../config"
|
import "../config"
|
||||||
|
import "../services"
|
||||||
|
|
||||||
// Workspace switcher for a single monitor. Works on both Sway/i3 and Hyprland:
|
// Workspace switcher for a single monitor. Three backends, one chip renderer
|
||||||
// the compositor is detected once from its instance env var, and the matching
|
// (WorkspaceChip):
|
||||||
// Quickshell backend model is used. The two backends expose the same item shape
|
//
|
||||||
// (name / active / focused / urgent / monitor.name / activate()), so a single
|
// * Sway/i3 and Hyprland expose the same item shape (name / active / focused
|
||||||
// chip renderer covers both. The `?:` on `model` only evaluates the taken
|
// / urgent / monitor.name / activate()), so both feed the same Repeater.
|
||||||
// branch, so the unused backend singleton is never instantiated (no stray IPC).
|
// * 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 {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
required property var screen // ShellScreen this bar lives on
|
required property var screen // ShellScreen this bar lives on
|
||||||
spacing: Theme.gap
|
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.
|
// Name of the output this bar is on, used to filter workspaces.
|
||||||
readonly property string monitorName: screen ? screen.name : ""
|
readonly property string monitorName: screen ? screen.name : ""
|
||||||
|
|
||||||
Repeater {
|
// attwm keeps tags, layout and geometry per output, so the tag row on this
|
||||||
model: root.isHyprland ? Hyprland.workspaces : I3.workspaces
|
// bar reflects this monitor rather than whichever one has focus.
|
||||||
|
readonly property var attwmOutput: Compositor.isAttwm
|
||||||
|
? Attwm.outputFor(monitorName)
|
||||||
|
: null
|
||||||
|
|
||||||
Rectangle {
|
// ── Sway/i3 and Hyprland ────────────────────────────────────────────────
|
||||||
id: chip
|
Repeater {
|
||||||
|
model: Compositor.isAttwm ? 0
|
||||||
|
: Compositor.isHyprland ? Hyprland.workspaces
|
||||||
|
: I3.workspaces
|
||||||
|
|
||||||
|
WorkspaceChip {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
// only show workspaces belonging to this monitor. Both I3Workspace
|
// only show workspaces belonging to this monitor. Both I3Workspace
|
||||||
// and HyprlandWorkspace expose .monitor as an object with a .name.
|
// and HyprlandWorkspace expose .monitor as an object with a .name.
|
||||||
visible: modelData.monitor && modelData.monitor.name === root.monitorName
|
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
|
label: modelData.name
|
||||||
readonly property bool isUrgent: modelData.urgent
|
active: modelData.active
|
||||||
|
focused: modelData.focused
|
||||||
|
urgent: modelData.urgent
|
||||||
|
|
||||||
color: isUrgent ? Theme.red
|
onClicked: modelData.activate()
|
||||||
: isFocused ? Theme.blue
|
}
|
||||||
: modelData.active ? Theme.surface1
|
}
|
||||||
: Theme.surface0
|
|
||||||
|
|
||||||
Text {
|
// ── attwm ───────────────────────────────────────────────────────────────
|
||||||
id: label
|
Repeater {
|
||||||
anchors.centerIn: parent
|
// 0 until the socket has produced a state object, so nothing is drawn
|
||||||
text: chip.modelData.name
|
// if we guessed the window manager wrong.
|
||||||
font.family: Theme.font
|
model: Compositor.isAttwm ? Attwm.tagCount : 0
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
font.bold: chip.isFocused || chip.isUrgent
|
|
||||||
color: (chip.isFocused || chip.isUrgent) ? Theme.mantle : Theme.text
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
WorkspaceChip {
|
||||||
anchors.fill: parent
|
id: tag
|
||||||
cursorShape: Qt.PointingHandCursor
|
required property int index
|
||||||
onClicked: chip.modelData.activate()
|
|
||||||
|
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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user