fixes waybar, adds quickshell

This commit is contained in:
2026-05-04 15:32:47 +02:00
parent 5e55b51220
commit bd17b76c30
34 changed files with 995 additions and 1217 deletions
+60
View File
@@ -0,0 +1,60 @@
// Bar.qml - top panel
// Neighbouring types (Pill, ClockWidget, Theme, etc.) are auto-imported by QuickShell.
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import "./modules"
PanelWindow {
id: root
WlrLayershell.namespace: "quickshell-bar"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
}
margins {
left: 2
right: 2
bottom: 1
top: 3
}
implicitHeight: Theme.barHeight
exclusiveZone: Theme.barHeight
color: "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
spacing: Theme.spacing
// ─── LEFT ──────────────────────────────────────────
ClockWidget {}
WeatherWidget {}
SysTrayWidget {}
WorkspacesWidget { screen: root.screen }
MediaWidget {}
WindowTitleWidget { screen: root.screen }
Item { Layout.fillWidth: true }
// ─── RIGHT ─────────────────────────────────────────
CavaWidget {}
AudioWidget {}
MemoryWidget {}
CpuWidget {}
TemperatureWidget {}
BatteryWidget {}
BluetoothWidget {}
PowerProfilesWidget {}
PowerMenuWidget {}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Exec.qml - fire-and-forget process launcher
// Usage from any file: Exec.run(["kitty", "-e", "btop"])
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
function run(cmd) {
const proc = procPool.createObject(root, { command: cmd });
proc.running = true;
}
Component {
id: procPool
Process {
running: false
onExited: destroy()
}
}
}
+39
View File
@@ -0,0 +1,39 @@
// Pill.qml - styled module container
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
default property alias content: inner.data
property bool hovered: mouseArea.containsMouse
signal clicked(var mouse)
signal scrolled(var wheel)
property real leftPadding: Theme.pillPadH
property real rightPadding: Theme.pillPadH
implicitWidth: inner.implicitWidth + leftPadding + rightPadding
implicitHeight: Theme.barHeight
radius: Theme.radius
color: hovered ? Theme.pillHover : Theme.pill
Behavior on color { ColorAnimation { duration: 150 } }
RowLayout {
id: inner
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
spacing: 4
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (m) => root.clicked(m)
onWheel: (w) => root.scrolled(w)
}
}
+38
View File
@@ -0,0 +1,38 @@
// Theme.qml - global palette & dimensions
// QuickShell auto-discovers this; access from any file as `Theme.colorN` etc.
pragma Singleton
import Quickshell
import QtQuick
Singleton {
// ── Wallust palette ──────────────────────────────────────
readonly property color background: "#252425"
readonly property color foreground: "#F9F1D9"
readonly property color color0: "#505051"
readonly property color color1: "#9C604E"
readonly property color color2: "#807A52"
readonly property color color3: "#908BAB"
readonly property color color4: "#B7815F"
readonly property color color5: "#B9BECA"
readonly property color color6: "#EED793"
readonly property color color7: "#EEE3C1"
readonly property color color8: "#A79F87"
// ── Derived / semantic ────────────────────────────────────
readonly property color pill: Qt.rgba(0.976, 0.945, 0.851, 0.15)
readonly property color pillHover: color2
readonly property color wsActive: color3
readonly property color wsUrgent: color1
// ── Typography ────────────────────────────────────────────
readonly property string fontSans: "Fira Sans Condensed"
readonly property string fontMono: "FiraCode Nerd Font"
readonly property int fontSize: 12
// ── Bar geometry ─────────────────────────────────────────
readonly property int barHeight: 28
readonly property int barPadding: 2
readonly property int radius: 5
readonly property int pillPadH: 10
readonly property int spacing: 4
}
@@ -0,0 +1,48 @@
// modules/AudioWidget.qml - wireplumber volume, matches waybar style.
// Icon set mirrors waybar wireplumber format-icons (NerdFont).
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import ".."
Pill {
id: root
property var node: Pipewire.defaultAudioSink
property bool muted: node?.audio.muted ?? false
property real vol: node?.audio.volume ?? 0
property string icon: {
if (muted || vol === 0) return " "
if (vol < 0.34) return " "
if (vol < 0.67) return " "
return " "
}
onClicked: (m) => {
if (m.button === Qt.LeftButton) Exec.run(["pavucontrol"])
if (m.button === Qt.RightButton && node)
node.audio.muted = !node.audio.muted
}
onScrolled: (w) => {
if (!node) return
var delta = w.angleDelta.y > 0 ? 0.04 : -0.04
node.audio.volume = Math.max(0, Math.min(1.5, node.audio.volume + delta))
}
// Cava feeds into this on the left → right border only
leftPadding: 0
rightPadding: Theme.pillPadH
Text {
text: root.icon
font { family: Theme.fontMono; pixelSize: Theme.fontSize; }
color: "#fab387" // peach accent matching waybar foreground color for icon
}
Text {
text: root.muted ? "muted" : Math.round(root.vol * 100) + "%"
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
}
@@ -0,0 +1,56 @@
// BatteryWidget.qml - battery icon + percentage
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property int capacity: 100
property string status: "Unknown"
property bool charging: status === "Charging"
property bool plugged: status === "Full" || status === "Not charging"
property bool critical: capacity <= 15 && !charging
property string icon: {
if (charging) return " "
if (plugged) return " "
if (capacity > 80) return ""
if (capacity > 60) return ""
if (capacity > 40) return ""
if (capacity > 20) return ""
return ""
}
Text {
text: root.icon + " " + root.capacity + "%"
font.family: Theme.fontSans
font.pixelSize: Theme.fontSize
color: root.critical ? Theme.color1 : Theme.foreground
}
Process {
id: batProc
running: false
command: ["bash", "-c",
"cat /sys/class/power_supply/BAT0/capacity 2>/dev/null; " +
"echo ---; " +
"cat /sys/class/power_supply/BAT0/status 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
const parts = this.text.split("---")
if (parts.length >= 2) {
const cap = parseInt(parts[0].trim())
if (!isNaN(cap)) root.capacity = cap
root.status = parts[1].trim()
}
}
}
onExited: batProc.running = false
}
Timer {
interval: 5000; running: true; repeat: true
triggeredOnStart: true
onTriggered: batProc.running = true
}
}
@@ -0,0 +1,61 @@
// modules/BluetoothWidget.qml - ᛒ status / device alias
// Uses bluetoothctl via a polled Process (no BlueZ QML bindings in QS yet).
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property string btStatus: "off" // "off" | "on" | "connected"
property string devAlias: ""
readonly property var bgColors: ({
"off": Qt.rgba(0.565, 0.545, 0.671, 0.3), // alpha(@color3, 0.3)
"on": Theme.color2,
"connected": Theme.color4,
})
// Override Pill's own color binding
color: bgColors[btStatus] ?? Theme.pill
onClicked: (m) => {
if (m.button === Qt.LeftButton)
Exec.run(["blueman-manager"])
}
Text {
text: {
var s = root.btStatus
if (s === "connected") return "ᛒ " + (root.devAlias || "connected")
if (s === "on") return "ᛒ on"
return "ᛒ off"
}
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
// Poll bluetoothctl show + info every 5 s
Process {
id: btProc
running: false
command: ["bash", "-c",
"bluetoothctl show | grep -E 'Powered|Name'; " +
"bluetoothctl info 2>/dev/null | grep -E 'Name|Connected'"]
stdout: SplitParser {
onRead: (line) => {
if (/Powered:\s+no/i.test(line)) { root.btStatus = "off"; root.devAlias = "" }
if (/Powered:\s+yes/i.test(line)) { if (root.btStatus === "off") root.btStatus = "on" }
if (/Connected:\s+yes/i.test(line)) root.btStatus = "connected"
if (/Connected:\s+no/i.test(line)) { if (root.btStatus === "connected") { root.btStatus = "on"; root.devAlias = "" } }
var match = /^\s+Name:\s+(.+)/.exec(line)
if (match && root.btStatus === "connected") root.devAlias = match[1].trim()
}
}
onExited: btProc.running = false
}
Timer {
interval: 5000; running: true; repeat: true
triggeredOnStart: true
onTriggered: btProc.running = true
}
}
+91
View File
@@ -0,0 +1,91 @@
// CavaWidget.qml - audio visualiser via cava raw output
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import ".."
Rectangle {
id: root
property var bars: Array(12).fill(0)
property bool silence: bars.every(v => v === 0)
readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"]
implicitWidth: cavaRow.implicitWidth + Theme.pillPadH * 2
implicitHeight: Theme.barHeight
radius: Theme.radius
color: cavaHover.containsMouse ? Theme.pillHover : Theme.pill
Behavior on color { ColorAnimation { duration: 150 } }
RowLayout {
id: cavaRow
anchors.centerIn: parent
spacing: 1
Repeater {
model: root.bars.length
Text {
required property int index
text: root.silence
? " "
: root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)]
font.family: Theme.fontMono
font.pixelSize: Theme.fontSize + 1
color: Theme.foreground
}
}
}
MouseArea {
id: cavaHover
anchors.fill: parent
hoverEnabled: true
onClicked: Exec.run(["pavucontrol"])
}
// Write the cava config once at startup, then run cava pointing at it.
Component.onCompleted: writeCfg.running = true
Process {
id: writeCfg
running: false
// Plain double-quoted string - no JS interpolation, bash sees ${VAR} verbatim.
command: ["bash", "-c",
"mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" +
"[general]\n" +
"framerate = 30\n" +
"bars = 12\n" +
"[input]\n" +
"method = pipewire\n" +
"source = auto\n" +
"[smoothing]\n" +
"noise_reduction = 77\n" +
"monstercat = 1\n" +
"[output]\n" +
"method = raw\n" +
"raw_target = /dev/stdout\n" +
"data_format = ascii\n" +
"ascii_max_range = 255\n" +
"bar_delimiter = 59\n" +
"CFG\n"
]
onExited: cavaProc.running = true
}
Process {
id: cavaProc
running: false
command: ["cava", "-p", "/tmp/qs-cava/cava.ini"]
stdout: SplitParser {
onRead: (line) => {
const parts = line.trim().replace(/;$/, "").split(";");
if (parts.length >= root.bars.length) {
root.bars = parts.slice(0, root.bars.length)
.map(v => parseInt(v) || 0);
}
}
}
onExited: cavaProc.running = true
}
}
@@ -0,0 +1,30 @@
// modules/ClockWidget.qml - " HH:MM DD Mon" (matches waybar clock format)
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import ".."
Pill {
onClicked: (m) => {
if (m.button === Qt.LeftButton)
Exec.run(["kitty", "-e", "calcure", "--class=float", "-T", "calcure"])
}
Text {
text: " " + Qt.formatDateTime(clock.now, "HH:mm") +
" " + Qt.formatDateTime(clock.now, "d MMM")
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
// Update every 10 s (no need for per-second ticks)
QtObject {
id: clock
property var now: new Date()
property var timer: Timer {
interval: 10000; running: true; repeat: true
triggeredOnStart: true
onTriggered: clock.now = new Date()
}
}
}
+55
View File
@@ -0,0 +1,55 @@
// CpuWidget.qml - "X.XGHz | Y%"
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property real freqGhz: 0
property int usagePct: 0
property int prevIdle: 0
property int prevTotal: 0
onClicked: (m) => {
if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
}
Text {
text: root.freqGhz.toFixed(1) + "GHz | " + root.usagePct + "%"
font.family: Theme.fontSans
font.pixelSize: Theme.fontSize
color: Theme.foreground
}
// /proc/stat - first line is total CPU
Process {
id: statProc
running: false
command: ["bash", "-c", "head -1 /proc/stat && cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
const lines = this.text.split("\n")
// line 0: cpu user nice system idle iowait irq softirq steal
const nums = lines[0].replace(/^cpu\s+/, "").split(/\s+/).map(Number)
const idle = (nums[3] || 0) + (nums[4] || 0)
const total = nums.reduce((s, v) => s + v, 0)
const dIdle = idle - root.prevIdle
const dTotal = total - root.prevTotal
if (dTotal > 0) root.usagePct = Math.round((1 - dIdle / dTotal) * 100)
root.prevIdle = idle
root.prevTotal = total
// line 1: current frequency in kHz
const khz = parseInt(lines[1] || "0")
if (!isNaN(khz) && khz > 0) root.freqGhz = khz / 1e6
}
}
onExited: statProc.running = false
}
Timer {
interval: 1500; running: true; repeat: true
triggeredOnStart: true
onTriggered: statProc.running = true
}
}
@@ -0,0 +1,54 @@
// modules/MediaWidget.qml
// Mirrors waybar custom/spotify - uses MPRIS via Quickshell.Services.Mpris.
// Shows: artist - title + spotify icon. Click to play/pause, scroll to skip.
import QtQuick
import Quickshell.Services.Mpris
import ".."
Pill {
id: root
// Pick the first active player (prefer spotify)
property MprisPlayer activePlayer: {
var players = Mpris.players.values
for (var i = 0; i < players.length; i++)
if (players[i].identity.toLowerCase() === "spotify") return players[i]
return players.length > 0 ? players[0] : null
}
property string trackText: {
if (!activePlayer) return ""
var p = activePlayer
var info = ""
if (p.trackArtists && p.trackTitle)
info = p.trackArtists.join(", ") + " - " + p.trackTitle
else if (p.trackTitle)
info = p.trackTitle
if (info.length > 45) info = info.substring(0, 45) + "..."
if (p.playbackState !== MprisPlaybackState.Playing && info)
info = " " + info
return info + " " // trailing Nerd Font Spotify icon
}
visible: trackText !== ""
Text {
text: root.trackText
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
elide: Text.ElideRight
maximumLineCount: 1
}
onClicked: (m) => {
if (!root.activePlayer) return
if (m.button === Qt.LeftButton)
root.activePlayer.togglePlaying()
}
onScrolled: (w) => {
if (!root.activePlayer) return
if (w.angleDelta.y > 0) root.activePlayer.next()
else root.activePlayer.previous()
}
}
@@ -0,0 +1,43 @@
// MemoryWidget.qml - " X.XX / Y GB"
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property real usedGb: 0
property real totalGb: 0
onClicked: (m) => {
if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
}
Text {
text: " " + root.usedGb.toFixed(2) + " / " + root.totalGb.toFixed(0) + " GB"
font.family: Theme.fontSans
font.pixelSize: Theme.fontSize
color: Theme.foreground
}
Process {
id: memProc
running: false
command: ["cat", "/proc/meminfo"]
stdout: StdioCollector {
onStreamFinished: {
const text = this.text
const total = parseInt((/MemTotal:\s+(\d+)/.exec(text) || [])[1] || "0")
const avail = parseInt((/MemAvailable:\s+(\d+)/.exec(text) || [])[1] || "0")
root.totalGb = total / 1048576
root.usedGb = (total - avail) / 1048576
}
}
onExited: memProc.running = false
}
Timer {
interval: 5000; running: true; repeat: true
triggeredOnStart: true
onTriggered: memProc.running = true
}
}
@@ -0,0 +1,83 @@
// modules/PowerMenuWidget.qml - ⏻ button with inline popup menu.
// Matches waybar custom/power with menu-actions.
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import ".."
Pill {
id: root
onClicked: (m) => {
if (m.button === Qt.LeftButton) menu.visible = !menu.visible
}
Text {
text: "⏻ "
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
// Inline drop-up popup - appears above the bar
Rectangle {
id: menu
visible: false
z: 100
width: 130
height: menuCol.implicitHeight + 16
radius: 5
color: Qt.rgba(0.086, 0.075, 0.125, 0.85)
border.color: Qt.rgba(1, 1, 1, 0.06)
border.width: 1
// Anchor above the pill
parent: root.parent // reparent to bar so z-ordering works
x: root.x + root.width - width
y: root.y - height - 4
ColumnLayout {
id: menuCol
anchors { fill: parent; margins: 8 }
spacing: 2
Repeater {
model: [
{ label: "Suspend", cmd: ["systemctl", "suspend"] },
{ label: "Hibernate", cmd: ["systemctl", "hibernate"] },
{ label: "Logout", cmd: ["hyprctl", "dispatch", "exit"] },
{ label: "Reboot", cmd: ["reboot"] },
{ label: "Shutdown", cmd: ["shutdown", "now"] },
]
delegate: Rectangle {
required property var modelData
Layout.fillWidth: true
height: 26
radius: 5
color: itemHover.containsMouse
? Theme.pillHover
: "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors { left: parent.left; verticalCenter: parent.verticalCenter; leftMargin: 8 }
text: modelData.label
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
MouseArea {
id: itemHover
anchors.fill: parent
hoverEnabled: true
onClicked: {
menu.visible = false
Exec.run(modelData.cmd)
}
}
}
}
}
}
}
@@ -0,0 +1,59 @@
// PowerProfilesWidget.qml - ⚡/⚖/🔋 + click-to-cycle
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property string profile: "balanced"
readonly property var profileOrder: ["performance", "balanced", "power-saver"]
readonly property var icons: ({
"performance": "⚡",
"balanced": "⚖",
"power-saver": "🔋",
})
onClicked: (m) => {
if (m.button !== Qt.LeftButton) return;
const i = profileOrder.indexOf(root.profile);
const next = profileOrder[(i + 1) % profileOrder.length];
setProc.command = ["powerprofilesctl", "set", next];
setProc.running = true;
root.profile = next; // optimistic update
}
Text {
text: (root.icons[root.profile] ?? "⚡")
font.family: Theme.fontSans
font.pixelSize: Theme.fontSize
color: Theme.foreground
}
// Read current profile periodically
Process {
id: readProc
running: false
command: ["powerprofilesctl", "get"]
stdout: SplitParser {
onRead: (line) => root.profile = line.trim()
}
onExited: readProc.running = false
}
// Setter - command is rewritten on each click
Process {
id: setProc
running: false
command: ["true"]
onExited: setProc.running = false
}
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: readProc.running = true
}
}
@@ -0,0 +1,63 @@
// modules/SysTrayWidget.qml - SNI system tray (nm-applet, blueman ...)
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
import ".."
Rectangle {
id: root
color: "transparent"
radius: Theme.radius
implicitWidth: trayRow.implicitWidth + 6
implicitHeight: Theme.barHeight
RowLayout {
id: trayRow
anchors.centerIn: parent
spacing: Theme.spacing
Repeater {
model: SystemTray.items
Rectangle {
required property SystemTrayItem modelData
width: 22; height: 22
radius: Theme.radius
color: trayHover.containsMouse
? Theme.pillHover
: Qt.rgba(0.976, 0.945, 0.851, 0.15)
Behavior on color { ColorAnimation { duration: 150 } }
Image {
anchors { fill: parent; margins: 4 }
source: modelData.icon
fillMode: Image.PreserveAspectFit
smooth: true
}
MouseArea {
id: trayHover
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (m) => {
if (m.button === Qt.LeftButton)
modelData.activate()
else
modelData.contextMenu(mapToGlobal(mouseX, mouseY))
}
}
// Attention indicator dot
Rectangle {
visible: modelData.status === SystemTrayItem.NeedsAttention
width: 5; height: 5; radius: 2.5
color: Theme.wsUrgent
anchors { bottom: parent.bottom; right: parent.right; margins: 1 }
}
}
}
}
}
@@ -0,0 +1,43 @@
// TemperatureWidget.qml - CPU package temperature
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property int tempC: 0
property bool critical: tempC >= 80
property string icon: tempC < 50 ? "" : tempC < 70 ? "" : ""
onClicked: (m) => {
if (m.button === Qt.LeftButton) Exec.run(["xsensors"])
}
Text {
text: root.icon + " " + root.tempC + "°C"
font.family: Theme.fontSans
font.pixelSize: Theme.fontSize
color: root.critical ? Theme.color1 : Theme.foreground
}
// Read first available CPU package sensor - works regardless of hwmon number
Process {
id: tempProc
running: false
command: ["bash", "-c", "cat /sys/class/hwmon/hwmon*/temp1_input 2>/dev/null | head -1"]
stdout: SplitParser {
onRead: (line) => {
const raw = parseInt(line.trim())
if (!isNaN(raw)) root.tempC = Math.round(raw / 1000)
}
}
onExited: tempProc.running = false
}
Timer {
interval: 4000; running: true; repeat: true
triggeredOnStart: true
onTriggered: tempProc.running = true
}
}
@@ -0,0 +1,34 @@
// modules/WeatherWidget.qml - wttr.in one-liner, refreshed hourly
import QtQuick
import Quickshell.Io
import ".."
Pill {
id: root
property string weatherText: "..."
Text {
text: weatherText
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
}
// ── Fetch via curl ────────────────────────────────────────
Process {
id: curl
command: ["curl", "-s", "--max-time", "8", "https://wttr.in/?format=1"]
running: false
stdout: SplitParser {
onRead: (line) => root.weatherText = line.trim()
}
onExited: curl.running = false
}
Timer {
interval: 3600000 // 1 hour
running: true
repeat: true
triggeredOnStart: true
onTriggered: curl.running = true
}
}
@@ -0,0 +1,28 @@
// modules/WindowTitleWidget.qml - hyprland/window equivalent
import QtQuick
import Quickshell.Hyprland
import ".."
Pill {
id: root
required property var screen
property string title: {
var ws = Hyprland.focusedWorkspace
if (!ws) return ""
var win = ws.lastWindow
if (!win) return ""
var t = win.title ?? ""
return t.length > 60 ? t.substring(0, 60) + "..." : t
}
visible: title !== ""
Text {
text: root.title
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
color: Theme.foreground
elide: Text.ElideRight
maximumLineCount: 1
}
}
@@ -0,0 +1,70 @@
// modules/WorkspacesWidget.qml
// Kanji workspace labels, per-monitor, matches waybar hyprland/workspaces.
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
Rectangle {
id: root
required property var screen
color: "transparent"
implicitWidth: wsRow.implicitWidth
implicitHeight: Theme.barHeight
// Filter workspaces that belong to this screen's monitor
property string monitorName: {
for (var i = 0; i < Hyprland.monitors.values.length; i++) {
var m = Hyprland.monitors.values[i]
if (m.name === screen.name) return m.name
}
return ""
}
RowLayout {
id: wsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacing
Repeater {
model: {
// sort visible workspaces for this monitor
var all = Hyprland.workspaces.values
return all.filter(ws => ws.monitor && ws.monitor.name === root.monitorName)
.sort((a, b) => a.id - b.id)
}
delegate: Rectangle {
required property var modelData
property bool isActive: modelData.id === (Hyprland.focusedWorkspace?.id ?? -1)
width: 32
height: Theme.barHeight - 4
radius: Theme.radius
Layout.alignment: Qt.AlignVCenter
color: isActive
? Theme.wsActive
: (wsBtn.containsMouse ? Theme.pillHover : Theme.pill)
Behavior on color { ColorAnimation { duration: 200 } }
Text {
anchors.centerIn: parent
text: Math.min(modelData.id - 1, 10 - 1)
font { family: Theme.fontSans; pixelSize: 11 }
color: Theme.foreground
}
MouseArea {
id: wsBtn
anchors.fill: parent
hoverEnabled: true
onClicked: Hyprland.dispatch("workspace " + modelData.id)
onWheel: (w) => Hyprland.dispatch(
"workspace " + (w.angleDelta.y > 0 ? "e+1" : "e-1"))
}
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// shell.qml - entry point
// QuickShell scans this directory and auto-imports neighbours (Bar, Theme, Exec, ...).
// Do NOT create a qmldir file - QuickShell synthesises one automatically.
import Quickshell
ShellRoot {
Variants {
model: Quickshell.screens
Bar {
required property var modelData
screen: modelData
}
}
}