update quickshell conf

This commit is contained in:
Johannes Knopp
2026-04-14 18:13:39 +02:00
parent 9248f9b33f
commit 6efa5d599c
10 changed files with 663 additions and 475 deletions

View File

@ -17,68 +17,121 @@ PanelWindow {
color: Theme.bg
readonly property string screenName: modelData.name
property string activePopup: ""
// Layout — three sections anchored independently for true centering
readonly property int bw: Theme.borderWidth // 2
readonly property int pad: Theme.enclosureMargin // 3
// ── Bar bottom border ─────────────────────────────────────────────
Rectangle {
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
height: root.bw
color: Theme.border
}
// ── Bar content ───────────────────────────────────────────────────
Item {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 2
anchors.bottomMargin: 2
// Left
Enclosure {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
child: Workspaces {
screenName: root.screenName
}
anchors {
fill: parent
leftMargin: 8
rightMargin: 8
topMargin: root.bw
bottomMargin: root.bw
}
// Center — truly centered regardless of left/right content width
Enclosure {
anchors.centerIn: parent
child: MusicPlayer {
id: musicChip
onClicked: musicControls.visible = !musicControls.visible
}
Workspaces {
anchors { left: parent.left; verticalCenter: parent.verticalCenter }
screenName: root.screenName
}
// Right — status chips
Enclosure {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
child: Row {
spacing: 8
MusicPlayer {
id: musicChip
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
onClicked: root.activePopup = root.activePopup === "controls" ? "" : "controls"
}
SysTray {
anchors.verticalCenter: parent.verticalCenter
barWindow: root
}
Row {
id: rightRow
anchors { right: parent.right; verticalCenter: parent.verticalCenter }
spacing: 8
NetworkStatus {
anchors.verticalCenter: parent.verticalCenter
}
VolumeControl {
anchors.verticalCenter: parent.verticalCenter
}
Clock {
anchors.verticalCenter: parent.verticalCenter
}
SysTray {
anchors.verticalCenter: parent.verticalCenter
barWindow: root
}
NetworkStatus {
anchors.verticalCenter: parent.verticalCenter
}
VolumeControl {
anchors.verticalCenter: parent.verticalCenter
onClickedLeft: root.activePopup = root.activePopup === "mixer" ? "" : "mixer"
}
Clock {
id: clockDisp
anchors.verticalCenter: parent.verticalCenter
onClicked: root.activePopup = root.activePopup === "calendar" ? "" : "calendar"
}
}
}
// Popup declared after the layout so musicChip is already initialised.
// Centered horizontally within the bar window, placed just below it.
MusicPlayerControls {
id: musicControls
visible: false
// ── Music controls popup ──────────────────────────────────────────
PopoutWindow {
popupName: "controls"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
anchor.rect.x: Math.round((root.width - implicitWidth) / 2)
implicitWidth: Math.max(musicChip.implicitWidth + 2 * root.pad,
ctrlContent.implicitWidth + 2 * root.bw)
implicitHeight: ctrlContent.implicitHeight + root.bw + Theme.radius
MusicPlayerControls {
id: ctrlContent
anchors { left: parent.left; right: parent.right }
player: musicChip.player
}
}
// ── Calendar popup ────────────────────────────────────────────────
PopoutWindow {
popupName: "calendar"
activePopup: root.activePopup
anchor.window: root
anchor.rect.x: Math.round((root.width - musicControls.width) / 2)
anchor.rect.y: root.height
player: musicChip.player
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
calContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: calContent.implicitHeight + root.bw + Theme.radius
CalendarContent {
id: calContent
anchors { left: parent.left; right: parent.right }
now: clockDisp.now
}
}
// ── Volume mixer popup ────────────────────────────────────────────
PopoutWindow {
popupName: "mixer"
activePopup: root.activePopup
anchor.window: root
anchor.rect.y: root.implicitHeight - root.bw - Theme.radius
readonly property real pw: Math.max(rightRow.width + 2 * root.pad,
mixContent.implicitWidth + 2 * root.bw)
anchor.rect.x: root.width - pw
implicitWidth: pw
implicitHeight: mixContent.implicitHeight + root.bw + Theme.radius
VolumeMixerContent {
id: mixContent
anchors { left: parent.left; right: parent.right }
}
}
}

View File

@ -0,0 +1,139 @@
import QtQuick
import "../components"
// Calendar grid a plain Item so it can live inside an expanding section border.
Item {
id: root
property var now: new Date()
property int viewYear: now.getFullYear()
property int viewMonth: now.getMonth() // 0-based
readonly property var monthNames: [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate()
// First weekday of month: Mon=0 … Sun=6
readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7
readonly property int todayYear: now.getFullYear()
readonly property int todayMonth: now.getMonth()
readonly property int todayDay: now.getDate()
// All widths derived from this constant — never from parent.width —
// to avoid Column polish loops when the popup is wider than the grid.
readonly property int cellW: 28
readonly property int cellGap: 2
readonly property int gridW: 7 * cellW + 6 * cellGap // 208
implicitWidth: gridW + 24
implicitHeight: calLayout.implicitHeight + 16
Column {
id: calLayout
// Centre the fixed-width grid inside the (potentially wider) popup.
// No left+right anchors → implicitWidth stays at gridW, no resize loop.
anchors { top: parent.top; topMargin: 8; horizontalCenter: parent.horizontalCenter }
spacing: 6
// Month navigation — all child widths are explicit constants
Row {
spacing: 0
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 0) { root.viewMonth = 11; root.viewYear-- }
else root.viewMonth--
}
}
}
Text {
text: root.monthNames[root.viewMonth] + " " + root.viewYear
color: Theme.text
font.pixelSize: 13
font.bold: true
width: root.gridW - 40 // fixed constant, not parent.width
horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMonth === 11) { root.viewMonth = 0; root.viewYear++ }
else root.viewMonth++
}
}
}
}
// Weekday headers
Row {
spacing: root.cellGap
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
text: modelData
color: Theme.textDim
font.pixelSize: 10
width: root.cellW
horizontalAlignment: Text.AlignHCenter
}
}
}
// Day grid
Grid {
columns: 7
spacing: root.cellGap
Repeater {
model: root.totalCells
delegate: Item {
width: root.cellW
height: 22
readonly property int dayNum: index - root.startOffset + 1
readonly property bool valid:
index >= root.startOffset &&
index < root.startOffset + root.daysInMonth
readonly property bool isToday:
valid &&
root.viewYear === root.todayYear &&
root.viewMonth === root.todayMonth &&
dayNum === root.todayDay
Rectangle {
anchors.fill: parent
radius: 3
color: isToday ? Theme.accent : "transparent"
visible: isToday
}
Text {
anchors.centerIn: parent
text: parent.valid ? String(parent.dayNum) : ""
color: parent.isToday ? Theme.text : Theme.textDim
font.pixelSize: 11
font.bold: parent.isToday
}
}
}
}
}
}

View File

@ -1,10 +1,12 @@
import Quickshell
import QtQuick
import "../components"
Item {
id: root
implicitWidth: timeLabel.implicitWidth + 10
signal clicked
implicitWidth: timeLabel.implicitWidth + 16
implicitHeight: 24
property var now: new Date()
@ -19,10 +21,13 @@ Item {
readonly property string display: {
const d = now
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
const h = String(d.getHours()).padStart(2, "0")
const m = String(d.getMinutes()).padStart(2, "0")
const s = String(d.getSeconds()).padStart(2, "0")
return days[d.getDay()] + " " + h + ":" + m + ":" + s
const dd = String(d.getDate()).padStart(2, "0")
const mm = String(d.getMonth() + 1).padStart(2, "0")
const yyyy = d.getFullYear()
const h = String(d.getHours()).padStart(2, "0")
const m = String(d.getMinutes()).padStart(2, "0")
const s = String(d.getSeconds()).padStart(2, "0")
return days[d.getDay()] + " " + dd + "." + mm + "." + yyyy + " " + h + ":" + m + ":" + s
}
Text {
@ -37,160 +42,6 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: calPopup.visible = !calPopup.visible
}
// --------------- Calendar popup ---------------
PopupWindow {
id: calPopup
visible: false
anchor.item: root
anchor.edges: Edges.Bottom
anchor.gravity: Edges.Bottom
// calendar state
property int viewYear: root.now.getFullYear()
property int viewMonth: root.now.getMonth() // 0-based
readonly property var monthNames: [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate()
// first weekday of month: Mon=0 … Sun=6
readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7
readonly property int todayYear: root.now.getFullYear()
readonly property int todayMonth: root.now.getMonth()
readonly property int todayDay: root.now.getDate()
implicitWidth: calBg.implicitWidth
implicitHeight: calBg.implicitHeight
Rectangle {
id: calBg
anchors.fill: parent
color: Theme.bg
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitWidth: calLayout.implicitWidth + 24
implicitHeight: calLayout.implicitHeight + 24
Column {
id: calLayout
anchors { fill: parent; margins: 12 }
spacing: 6
// Month navigation
Row {
width: parent.width
spacing: 0
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (calPopup.viewMonth === 0) {
calPopup.viewMonth = 11
calPopup.viewYear--
} else {
calPopup.viewMonth--
}
}
}
}
Text {
text: calPopup.monthNames[calPopup.viewMonth] + " " + calPopup.viewYear
color: Theme.text
font.pixelSize: 13
font.bold: true
// fill remaining space between arrows
width: parent.width - 40
horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""
color: Theme.text
font.pixelSize: 16
width: 20
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (calPopup.viewMonth === 11) {
calPopup.viewMonth = 0
calPopup.viewYear++
} else {
calPopup.viewMonth++
}
}
}
}
}
// Weekday headers
Row {
spacing: 2
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
text: modelData
color: Theme.textDim
font.pixelSize: 10
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
}
// Day grid
Grid {
columns: 7
spacing: 2
Repeater {
model: calPopup.totalCells
delegate: Item {
width: 28
height: 22
readonly property int dayNum: index - calPopup.startOffset + 1
readonly property bool valid:
index >= calPopup.startOffset &&
index < calPopup.startOffset + calPopup.daysInMonth
readonly property bool isToday:
valid &&
calPopup.viewYear === calPopup.todayYear &&
calPopup.viewMonth === calPopup.todayMonth &&
dayNum === calPopup.todayDay
Rectangle {
anchors.fill: parent
radius: 3
color: isToday ? Theme.accent : "transparent"
visible: isToday
}
Text {
anchors.centerIn: parent
text: parent.valid ? String(parent.dayNum) : ""
color: parent.isToday ? Theme.text : Theme.textDim
font.pixelSize: 11
font.bold: parent.isToday
}
}
}
}
}
}
onClicked: root.clicked()
}
}

View File

@ -1,4 +1,3 @@
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import "../components"

View File

@ -1,138 +1,124 @@
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import "../components"
PopupWindow {
// Plain Item lives inside the expanding center section border in Bar.qml.
Item {
id: root
required property var player
implicitWidth: 280
implicitHeight: bg.implicitHeight
implicitWidth: 280
implicitHeight: layout.implicitHeight + 28
// Tickle positionChanged every frame while playing so the player.position
// binding re-evaluates. Paused while the user is dragging the seek bar.
// Keep the progress binding live while playing, pause while seeking.
FrameAnimation {
running: (root.player?.isPlaying ?? false) && !progressArea.pressed
onTriggered: root.player.positionChanged()
}
Rectangle {
id: bg
anchors.fill: parent
color: Theme.bg
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitHeight: layout.implicitHeight + 28
ColumnLayout {
id: layout
anchors { fill: parent; margins: 14 }
spacing: 8
ColumnLayout {
id: layout
anchors { fill: parent; margins: 14 }
spacing: 8
// Track info
Text {
text: root.player?.trackTitle ?? ""
color: Theme.text
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
// Track info
Text {
text: root.player?.trackTitle ?? ""
color: Theme.text
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: root.player?.trackArtist ?? ""
color: Theme.textDim
font.pixelSize: 11
Layout.fillWidth: true
elide: Text.ElideRight
visible: (root.player?.trackArtist ?? "") !== ""
Layout.topMargin: -4
}
Text {
text: root.player?.trackArtist ?? ""
color: Theme.textDim
font.pixelSize: 11
Layout.fillWidth: true
elide: Text.ElideRight
visible: (root.player?.trackArtist ?? "") !== ""
Layout.topMargin: -4
}
// Progress bar
Item {
visible: root.player?.positionSupported && root.player?.lengthSupported
&& (root.player?.length ?? 0) > 0
Layout.fillWidth: true
height: 12
// Progress bar
Item {
visible: root.player?.positionSupported && root.player?.lengthSupported
&& (root.player?.length ?? 0) > 0
Layout.fillWidth: true
height: 12 // visual track is centred inside; extra height = easier to hit
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
// Track
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
// Filled portion
Rectangle {
width: root.player?.length > 0
? parent.width * (root.player.position / root.player.length)
: 0
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
id: progressArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) {
if (!(root.player?.canSeek)) return
const ratio = Math.max(0, Math.min(1, mx / width))
root.player.position = ratio * (root.player?.length ?? 0)
}
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
width: root.player?.length > 0
? parent.width * (root.player.position / root.player.length)
: 0
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
// Media controls
RowLayout {
Layout.fillWidth: true
spacing: 0
MouseArea {
id: progressArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Item { Layout.fillWidth: true }
ControlButton {
icon: "⏮"
iconSize: 18
enabled: root.player?.canGoPrevious ?? false
onActivated: root.player.previous()
function seek(mx) {
if (!(root.player?.canSeek)) return
const ratio = Math.max(0, Math.min(1, mx / width))
root.player.position = ratio * (root.player?.length ?? 0)
}
Item { width: 8 }
ControlButton {
icon: root.player?.isPlaying ? "⏸" : "▶"
iconSize: 22
enabled: true
onActivated: root.player?.togglePlaying()
}
Item { width: 8 }
ControlButton {
icon: "⏭"
iconSize: 18
enabled: root.player?.canGoNext ?? false
onActivated: root.player.next()
}
Item { Layout.fillWidth: true }
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
// Media controls
RowLayout {
Layout.fillWidth: true
spacing: 0
Item { Layout.fillWidth: true }
ControlButton {
icon: "⏮"
iconSize: 18
enabled: root.player?.canGoPrevious ?? false
onActivated: root.player.previous()
}
Item { width: 8 }
ControlButton {
icon: root.player?.isPlaying ? "⏸" : "▶"
iconSize: 22
enabled: true
onActivated: root.player?.togglePlaying()
}
Item { width: 8 }
ControlButton {
icon: "⏭"
iconSize: 18
enabled: root.player?.canGoNext ?? false
onActivated: root.player.next()
}
Item { Layout.fillWidth: true }
}
}
// Inline helper — a button that highlights on hover
component ControlButton: Rectangle {
id: btn
required property string icon
@ -144,7 +130,6 @@ PopupWindow {
implicitHeight: label.implicitHeight + 10
radius: Theme.radius
color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent'
Behavior on color { ColorAnimation { duration: 80 } }
Text {

View File

@ -0,0 +1,162 @@
import Quickshell
import QtQuick
import QtQuick.Shapes
import "../components"
PopupWindow {
id: self
property string popupName: ""
property string activePopup: ""
visible: false
color: "transparent"
default property alias content: contentSlot.data
readonly property int bw: Theme.borderWidth
readonly property real r: Theme.radius
readonly property bool _open: activePopup === popupName
on_OpenChanged: {
if (_open) {
hideTimer.stop();
self.visible = true;
fade.opacity = 1.0;
} else {
fade.opacity = 0.0;
hideTimer.start();
}
}
Timer {
id: hideTimer
interval: 200
onTriggered: self.visible = false
}
Item {
id: fade
anchors.fill: parent
opacity: 0.0
Behavior on opacity {
NumberAnimation {
duration: 180
easing.type: Easing.InOutCubic
}
}
// Background + border in one Shape so fill matches stroke exactly
Shape {
id: pbs
anchors.fill: parent
ShapePath {
strokeWidth: self.bw
strokeColor: Theme.border
fillColor: Theme.bg
// Start flush at top-left, hidden under bar
startX: 0
startY: self.r
// Left side down to bottom-left corner
PathLine {
x: 0
y: pbs.height - self.r
}
// Bottom-left corner — Counterclockwise for outward curve
PathArc {
x: self.r
y: pbs.height
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Bottom edge
PathLine {
x: pbs.width - self.r
y: pbs.height
}
// Bottom-right corner — Counterclockwise for outward curve
PathArc {
x: pbs.width
y: pbs.height - self.r
radiusX: self.r
radiusY: self.r
direction: PathArc.Counterclockwise
}
// Right side up, flush to top, hidden under bar
PathLine {
x: pbs.width
y: self.r
}
}
}
// // Top-left concave corner
// Shape {
// x: 0
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: 0
// startY: 0
// PathLine {
// x: self.r
// y: 0
// }
// PathArc {
// x: 0
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Clockwise
// }
// }
// }
//
// // Top-right concave corner
// Shape {
// x: parent.width - self.r
// y: 0
// width: self.r
// height: self.r
// ShapePath {
// fillColor: Theme.bg
// strokeColor: "transparent"
// startX: self.r
// startY: 0
// PathLine {
// x: 0
// y: 0
// }
// PathArc {
// x: self.r
// y: self.r
// radiusX: self.r
// radiusY: self.r
// direction: PathArc.Counterclockwise
// }
// }
// }
Item {
id: contentSlot
anchors {
top: parent.top
topMargin: self.r + self.bw
left: parent.left
leftMargin: self.bw
right: parent.right
rightMargin: self.bw
}
}
}
}

View File

@ -1,4 +1,3 @@
import Quickshell
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
@ -9,13 +8,12 @@ Item {
implicitWidth: chip.implicitWidth + 10
implicitHeight: 24
// Track all PipeWire nodes so their properties are populated.
// Without this, nodes in Pipewire.nodes are "unbound" and have null audio/type.
signal clickedLeft
PwObjectTracker {
objects: Pipewire.nodes.values
}
// ── helpers ────────────────────────────────────────────────────────
function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume
@ -26,13 +24,12 @@ Item {
node.audio.volume = Math.max(0, Math.min(1, v))
}
function volIcon(vol, muted) {
if (muted) return "\uf6a9" // fa-volume-mute
if (vol > 0.6) return "\uf028" // fa-volume-up
if (vol > 0.2) return "\uf027" // fa-volume-down
return "\uf026" // fa-volume-off
if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027"
return "\uf026"
}
// ── bar chip (default sink) ─────────────────────────────────────────
readonly property var defaultSink: Pipewire.defaultAudioSink
readonly property real defaultVolume: safeVolume(defaultSink)
readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false
@ -66,7 +63,7 @@ Item {
if (root.defaultSink?.ready && root.defaultSink?.audio)
root.defaultSink.audio.muted = !root.defaultSink.audio.muted
} else {
mixerPopup.visible = !mixerPopup.visible
root.clickedLeft()
}
}
onWheel: wheel => {
@ -74,149 +71,4 @@ Item {
root.setVolume(root.defaultSink, root.defaultVolume + wheel.angleDelta.y / 120 * 0.05)
}
}
// ── mixer popup ─────────────────────────────────────────────────────
PopupWindow {
id: mixerPopup
visible: false
anchor.item: root
anchor.edges: Edges.Bottom
anchor.gravity: Edges.Bottom
implicitWidth: mixerBg.implicitWidth
implicitHeight: mixerBg.implicitHeight
Rectangle {
id: mixerBg
anchors.fill: parent
color: Theme.bg
border.color: Theme.border
border.width: Theme.borderWidth
radius: Theme.radius
implicitWidth: 300
implicitHeight: mixerCol.implicitHeight + 24
ColumnLayout {
id: mixerCol
anchors { fill: parent; margins: 12 }
spacing: 4
// ── Output devices ──────────────────────────────────────
Text {
text: "Output Devices"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isSink && !modelData.isStream && modelData.ready
}
}
// ── Application streams ────────────────────────────────
Text {
text: "Applications"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
// exclude monitor sources; include only audio output streams
visible: modelData.isStream && !modelData.isSink && modelData.ready
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
}
}
}
}
}
// ── per-node row component ──────────────────────────────────────────
component NodeRow: RowLayout {
id: row
required property var node
spacing: 8
implicitHeight: 28
// mute toggle
Text {
id: nodeIcon
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.node.ready && row.node.audio)
row.node.audio.muted = !row.node.audio.muted
}
}
}
// name
Text {
text: row.node.description || row.node.nickname || row.node.name || "?"
font.pixelSize: 11
color: Theme.textDim
Layout.preferredWidth: 90
elide: Text.ElideRight
}
// horizontal slider
Item {
Layout.fillWidth: true
implicitHeight: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: parent.width * Math.min(1, root.safeVolume(row.node))
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) {
root.setVolume(row.node, mx / width)
}
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
// percentage
Text {
text: Math.round(root.safeVolume(row.node) * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
}
}

View File

@ -0,0 +1,143 @@
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import "../components"
// Volume mixer a plain Item so it can live inside an expanding section border.
Item {
id: root
implicitWidth: 300
implicitHeight: mixerCol.implicitHeight + 24
PwObjectTracker {
objects: Pipewire.nodes.values
}
function safeVolume(node) {
if (!node || !node.ready || !node.audio) return 0
const v = node.audio.volume
return (v !== undefined && !isNaN(v)) ? v : 0
}
function setVolume(node, v) {
if (!node || !node.ready || !node.audio) return
node.audio.volume = Math.max(0, Math.min(1, v))
}
function volIcon(vol, muted) {
if (muted) return "\uf6a9"
if (vol > 0.6) return "\uf028"
if (vol > 0.2) return "\uf027"
return "\uf026"
}
ColumnLayout {
id: mixerCol
anchors { fill: parent; margins: 12 }
spacing: 4
Text {
text: "Output Devices"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isSink && !modelData.isStream && modelData.ready
}
}
Text {
text: "Applications"
color: Theme.textDim
font.pixelSize: 10
Layout.fillWidth: true
Layout.topMargin: 6
Layout.bottomMargin: 2
}
Repeater {
model: Pipewire.nodes
delegate: NodeRow {
required property var modelData
Layout.fillWidth: true
node: modelData
visible: modelData.isStream && !modelData.isSink && modelData.ready
&& !(modelData.description ?? "").toLowerCase().includes("monitor")
}
}
}
component NodeRow: RowLayout {
id: row
required property var node
spacing: 8
implicitHeight: 28
Text {
text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false)
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 13
color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (row.node.ready && row.node.audio)
row.node.audio.muted = !row.node.audio.muted
}
}
}
Text {
text: row.node.description || row.node.nickname || row.node.name || "?"
font.pixelSize: 11
color: Theme.textDim
Layout.preferredWidth: 90
elide: Text.ElideRight
}
Item {
Layout.fillWidth: true
implicitHeight: 16
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 3
radius: 2
color: Theme.progressTrack
Rectangle {
width: parent.width * Math.min(1, root.safeVolume(row.node))
height: parent.height
radius: parent.radius
color: Theme.accent
}
}
MouseArea {
anchors { fill: parent; topMargin: -4; bottomMargin: -4 }
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function seek(mx) { root.setVolume(row.node, mx / width) }
onClicked: seek(mouseX)
onPositionChanged: if (pressed) seek(mouseX)
}
}
Text {
text: Math.round(root.safeVolume(row.node) * 100) + "%"
font.pixelSize: 10
color: Theme.textDim
Layout.preferredWidth: 30
horizontalAlignment: Text.AlignRight
}
}
}

View File

@ -1,9 +1,12 @@
Bar 1.0 Bar.qml
PopoutWindow 1.0 PopoutWindow.qml
CalendarContent 1.0 CalendarContent.qml
Clock 1.0 Clock.qml
MusicPlayer 1.0 MusicPlayer.qml
MusicPlayerControls 1.0 MusicPlayerControls.qml
NetworkStatus 1.0 NetworkStatus.qml
SysTray 1.0 SysTray.qml
VolumeControl 1.0 VolumeControl.qml
VolumeMixerContent 1.0 VolumeMixerContent.qml
Workspaces 1.0 Workspaces.qml
WorkspaceButton 1.0 WorkspaceButton.qml

View File

@ -3,7 +3,8 @@ import QtQuick
QtObject {
// Core palette
readonly property color bg: '#000000'
readonly property color bg: '#D9000000' // bar background (semi-transparent)
readonly property color bgPopup: '#000000' // popup background (fully opaque)
readonly property color accent: '#9B1A1A'
readonly property color border: '#FFFFFF'
readonly property color text: '#FFFFFF'
@ -12,7 +13,7 @@ QtObject {
readonly property color progressTrack: '#333333'
// Shape / sizing
readonly property int radius: 4
readonly property int radius: 16
readonly property int borderWidth: 2
readonly property int enclosureMargin: 3
}