1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Use integrated pipewire from quickshell

This commit is contained in:
bbedward
2025-07-15 15:26:01 -04:00
parent b7da76147f
commit 9035cb750b
7 changed files with 289 additions and 365 deletions

View File

@@ -68,7 +68,6 @@ Singleton {
function parseSettings(content) {
try {
console.log("Settings file content:", content)
if (content && content.trim()) {
var settings = JSON.parse(content)
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0

View File

@@ -1,332 +1,264 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool sinkMuted: sink?.audio?.muted ?? false
readonly property bool sourceMuted: source?.audio?.muted ?? false
readonly property real volumeLevel: (sink?.audio?.volume ?? 0) * 100
readonly property real micLevel: (source?.audio?.volume ?? 0) * 100
signal audioVolumeChanged(real volume)
signal audioMicLevelChanged(real level)
signal audioMuteChanged(bool muted)
signal audioMicMuteChanged(bool muted)
signal audioDeviceChanged()
property int volumeLevel: 50
onVolumeLevelChanged: audioVolumeChanged(volumeLevel)
onMicLevelChanged: audioMicLevelChanged(micLevel)
onSinkMutedChanged: audioMuteChanged(sinkMuted)
onSourceMutedChanged: audioMicMuteChanged(sourceMuted)
onSinkChanged: {
audioDeviceChanged()
}
onSourceChanged: {
audioDeviceChanged()
}
property var audioSinks: []
property string currentAudioSink: ""
property int micLevel: 50
property var audioSources: []
property string currentAudioSource: ""
property bool deviceScanningEnabled: false
property bool initialScanComplete: false
Process {
id: volumeChecker
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.volumeLevel = Math.min(100, parseInt(data.trim()) || 50)
}
Component.onCompleted: {
Qt.callLater(updateDevices)
}
function updateDevices() {
updateAudioSinks()
updateAudioSources()
}
Connections {
target: Pipewire
function onReadyChanged() {
if (Pipewire.ready) {
updateAudioSinks()
updateAudioSources()
}
}
}
Process {
id: micLevelChecker
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.micLevel = Math.min(100, parseInt(data.trim()) || 50)
}
}
function onDefaultAudioSinkChanged() {
updateAudioSinks()
}
function onDefaultAudioSourceChanged() {
updateAudioSources()
}
}
Process {
id: audioSinkLister
command: ["pactl", "list", "sinks"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let sinks = []
let lines = text.trim().split('\n')
let currentSink = null
for (let line of lines) {
line = line.trim()
if (line.startsWith('Sink #')) {
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
}
let sinkId = line.replace('Sink #', '').trim()
currentSink = {
id: sinkId,
name: "",
displayName: "",
description: "",
nick: "",
active: false
}
}
else if (line.startsWith('Name: ') && currentSink) {
currentSink.name = line.replace('Name: ', '').trim()
}
else if (line.startsWith('Description: ') && currentSink) {
currentSink.description = line.replace('Description: ', '').trim()
}
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim()
}
else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
}
}
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
}
for (let sink of sinks) {
let displayName = sink.description
if (!displayName || displayName === sink.name) {
displayName = sink.nick
}
if (!displayName || displayName === sink.name) {
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
else if (sink.name.includes("usb")) displayName = "USB Audio"
else if (sink.name.includes("hdmi")) displayName = "HDMI Audio"
else if (sink.name.includes("easyeffects")) displayName = "EasyEffects"
else displayName = sink.name
}
sink.displayName = displayName
}
root.audioSinks = sinks
defaultSinkChecker.running = true
}
}
}
}
Process {
id: audioSourceLister
command: ["pactl", "list", "sources"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let sources = []
let lines = text.trim().split('\n')
let currentSource = null
for (let line of lines) {
line = line.trim()
if (line.startsWith('Source #')) {
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
}
currentSource = {
id: line.replace('Source #', '').replace(':', ''),
name: '',
displayName: '',
active: false
}
}
else if (line.startsWith('Name: ') && currentSource) {
currentSource.name = line.replace('Name: ', '')
}
else if (line.startsWith('Description: ') && currentSource) {
let desc = line.replace('Description: ', '')
currentSource.displayName = desc
}
}
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
}
sources = sources.filter(source => !source.name.includes('.monitor'))
root.audioSources = sources
defaultSourceChecker.running = true
}
}
}
}
Process {
id: defaultSinkChecker
command: ["pactl", "get-default-sink"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.currentAudioSink = data.trim()
let updatedSinks = []
for (let sink of root.audioSinks) {
updatedSinks.push({
id: sink.id,
name: sink.name,
displayName: sink.displayName,
active: sink.name === root.currentAudioSink
})
}
root.audioSinks = updatedSinks
}
}
}
}
Process {
id: defaultSourceChecker
command: ["pactl", "get-default-source"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.currentAudioSource = data.trim()
let updatedSources = []
for (let source of root.audioSources) {
updatedSources.push({
id: source.id,
name: source.name,
displayName: source.displayName,
active: source.name === root.currentAudioSource
})
}
root.audioSources = updatedSources
}
}
}
}
function setVolume(percentage) {
let volumeSetProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "' + percentage + '%"]
running: true
onExited: volumeChecker.running = true
}
', root)
}
function setMicLevel(percentage) {
let micSetProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["pactl", "set-source-volume", "@DEFAULT_SOURCE@", "' + percentage + '%"]
running: true
onExited: micLevelChecker.running = true
}
', root)
}
function setAudioSink(sinkName) {
console.log("Setting audio sink to:", sinkName)
sinkSetProcess.command = ["pactl", "set-default-sink", sinkName]
sinkSetProcess.running = true
}
Process {
id: sinkSetProcess
running: false
onExited: (exitCode) => {
console.log("Audio sink change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio sink changed successfully")
defaultSinkChecker.running = true
if (root.deviceScanningEnabled) {
audioSinkLister.running = true
}
} else {
console.error("Failed to change audio sink")
}
}
}
function setAudioSource(sourceName) {
console.log("Setting audio source to:", sourceName)
sourceSetProcess.command = ["pactl", "set-default-source", sourceName]
sourceSetProcess.running = true
}
Process {
id: sourceSetProcess
running: false
onExited: (exitCode) => {
console.log("Audio source change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio source changed successfully")
defaultSourceChecker.running = true
if (root.deviceScanningEnabled) {
audioSourceLister.running = true
}
} else {
console.error("Failed to change audio source")
}
}
}
// Timer to check for node changes since ObjectModel doesn't expose change signals
Timer {
interval: 5000
running: root.deviceScanningEnabled && root.initialScanComplete
interval: 2000
running: Pipewire.ready
repeat: true
onTriggered: {
if (root.deviceScanningEnabled) {
audioSinkLister.running = true
audioSourceLister.running = true
if (Pipewire.nodes && Pipewire.nodes.values) {
let currentCount = Pipewire.nodes.values.length
if (currentCount !== lastNodeCount) {
lastNodeCount = currentCount
updateAudioSinks()
updateAudioSources()
}
}
}
}
property int lastNodeCount: 0
function updateAudioSinks() {
if (!Pipewire.ready || !Pipewire.nodes) return
let sinks = []
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node) continue
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
let displayName = getDisplayName(node)
sinks.push({
id: node.id.toString(),
name: node.name,
displayName: displayName,
subtitle: getDeviceSubtitle(node.name),
active: node === root.sink,
node: node
})
}
}
}
audioSinks = sinks
}
function updateAudioSources() {
if (!Pipewire.ready || !Pipewire.nodes) return
let sources = []
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node) continue
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream && !node.name.includes('.monitor')) {
sources.push({
id: node.id.toString(),
name: node.name,
displayName: getDisplayName(node),
subtitle: getDeviceSubtitle(node.name),
active: node === root.source,
node: node
})
}
}
}
audioSources = sources
}
function getDisplayName(node) {
// Check properties first (this is key for Bluetooth devices!)
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
if (node.description && node.description !== node.name) {
return node.description
}
if (node.nickname && node.nickname !== node.name) {
return node.nickname
}
// Fallback to name processing
if (node.name.includes("analog-stereo")) return "Built-in Speakers"
else if (node.name.includes("bluez")) return "Bluetooth Audio"
else if (node.name.includes("usb")) return "USB Audio"
else if (node.name.includes("hdmi")) return "HDMI Audio"
return node.name
}
function getDeviceSubtitle(nodeName) {
if (!nodeName) return ""
// Simple subtitle based on node name patterns
if (nodeName.includes('usb-')) {
if (nodeName.includes('SteelSeries')) {
return "USB Gaming Headset"
} else if (nodeName.includes('Generic')) {
return "USB Audio Device"
}
return "USB Audio"
} else if (nodeName.includes('pci-')) {
if (nodeName.includes('01_00.1') || nodeName.includes('01:00.1')) {
return "NVIDIA GPU Audio"
}
return "PCI Audio"
} else if (nodeName.includes('bluez')) {
return "Bluetooth Audio"
} else if (nodeName.includes('analog')) {
return "Built-in Audio"
}
return ""
}
readonly property string currentAudioSink: sink?.name ?? ""
readonly property string currentAudioSource: source?.name ?? ""
Component.onCompleted: {
console.log("AudioService: Starting initialization...")
audioSinkLister.running = true
audioSourceLister.running = true
initialScanComplete = true
console.log("AudioService: Initialization complete")
readonly property string currentSinkDisplayName: {
if (!sink) return ""
for (let sinkInfo of audioSinks) {
if (sinkInfo.node === sink) {
return sinkInfo.displayName
}
}
return sink.description || sink.name
}
function enableDeviceScanning(enabled) {
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
root.deviceScanningEnabled = enabled
if (enabled && root.initialScanComplete) {
audioSinkLister.running = true
audioSourceLister.running = true
readonly property string currentSourceDisplayName: {
if (!source) return ""
for (let sourceInfo of audioSources) {
if (sourceInfo.node === source) {
return sourceInfo.displayName
}
}
return source.description || source.name
}
function setVolume(percentage) {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false
sink.audio.volume = percentage / 100
}
}
function refreshDevices() {
console.log("AudioService: Manual device refresh triggered")
audioSinkLister.running = true
audioSourceLister.running = true
function setMicLevel(percentage) {
if (source?.ready && source?.audio) {
source.audio.muted = false
source.audio.volume = percentage / 100
}
}
function toggleMute() {
if (sink?.ready && sink?.audio) {
sink.audio.muted = !sink.audio.muted
}
}
function toggleMicMute() {
if (source?.ready && source?.audio) {
source.audio.muted = !source.audio.muted
}
}
function setAudioSink(sinkName) {
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (node && node.name === sinkName && (node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
Pipewire.preferredDefaultAudioSink = node
break
}
}
}
}
function setAudioSource(sourceName) {
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (node && node.name === sourceName && (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream) {
Pipewire.preferredDefaultAudioSource = node
break
}
}
}
}
PwObjectTracker {
id: nodeTracker
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
}

View File

@@ -11,13 +11,14 @@ Item {
property int audioSubTab: 0 // 0: Output, 1: Input
// These should be bound from parent
property real volumeLevel: 50
property real micLevel: 50
property string currentAudioSink: ""
property string currentAudioSource: ""
property var audioSinks: []
property var audioSources: []
readonly property real volumeLevel: AudioService.volumeLevel
readonly property real micLevel: AudioService.micLevel
readonly property bool volumeMuted: AudioService.sinkMuted
readonly property bool micMuted: AudioService.sourceMuted
readonly property string currentAudioSink: AudioService.currentAudioSink
readonly property string currentAudioSource: AudioService.currentAudioSource
readonly property var audioSinks: AudioService.audioSinks
readonly property var audioSources: AudioService.audioSources
Column {
anchors.fill: parent
@@ -102,11 +103,18 @@ Item {
spacing: Theme.spacingM
Text {
text: "volume_down"
text: audioTab.volumeMuted ? "volume_off" : "volume_down"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute()
}
}
Rectangle {
@@ -179,13 +187,6 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: audioTab.volumeLevel + "%"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Output Devices
@@ -224,14 +225,7 @@ Item {
}
Text {
text: "Current: " + (function() {
for (let sink of audioTab.audioSinks) {
if (sink.name === audioTab.currentAudioSink) {
return sink.displayName
}
}
return audioTab.currentAudioSink
})()
text: "Current: " + (AudioService.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
@@ -283,10 +277,16 @@ Item {
}
Text {
text: modelData.active ? "Selected" : ""
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
visible: modelData.active
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
@@ -298,8 +298,6 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("Clicked audio device:", JSON.stringify(modelData))
console.log("Device name to set:", modelData.name)
AudioService.setAudioSink(modelData.name)
}
}
@@ -337,11 +335,18 @@ Item {
spacing: Theme.spacingM
Text {
text: "mic"
text: audioTab.micMuted ? "mic_off" : "mic"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
color: audioTab.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
}
}
Rectangle {
@@ -415,12 +420,6 @@ Item {
}
}
Text {
text: audioTab.micLevel + "%"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Input Devices
@@ -459,14 +458,7 @@ Item {
}
Text {
text: "Current: " + (function() {
for (let source of audioTab.audioSources) {
if (source.name === audioTab.currentAudioSource) {
return source.displayName
}
}
return audioTab.currentAudioSource
})()
text: "Current: " + (AudioService.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
@@ -517,10 +509,16 @@ Item {
}
Text {
text: modelData.active ? "Selected" : ""
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
visible: modelData.active
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
@@ -532,8 +530,6 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("Clicked audio source:", JSON.stringify(modelData))
console.log("Source name to set:", modelData.name)
AudioService.setAudioSource(modelData.name)
}
}

View File

@@ -676,14 +676,6 @@ PanelWindow {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: controlCenterPopup.currentTab === "audio"
// Bind properties from root
volumeLevel: root.volumeLevel
micLevel: root.micLevel
currentAudioSink: root.currentAudioSink
currentAudioSource: root.currentAudioSource
audioSinks: root.audioSinks
audioSources: root.audioSources
}
// Bluetooth Tab

View File

@@ -8,6 +8,7 @@ Rectangle {
property string networkStatus: "disconnected"
property string wifiSignalStrength: "good"
property int volumeLevel: 50
property bool volumeMuted: false
property bool bluetoothAvailable: false
property bool bluetoothEnabled: false
property bool isActive: false
@@ -62,7 +63,7 @@ Rectangle {
// Audio Icon
Text {
text: root.volumeLevel === 0 ? "volume_off" :
text: root.volumeMuted ? "volume_off" :
root.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8

View File

@@ -41,6 +41,7 @@ PanelWindow {
property string networkStatus: "disconnected"
property string wifiSignalStrength: "good"
property int volumeLevel: 50
property bool volumeMuted: false
property bool bluetoothAvailable: false
property bool bluetoothEnabled: false
@@ -321,6 +322,7 @@ PanelWindow {
networkStatus: topBar.networkStatus
wifiSignalStrength: topBar.wifiSignalStrength
volumeLevel: topBar.volumeLevel
volumeMuted: topBar.volumeMuted
bluetoothAvailable: topBar.bluetoothAvailable
bluetoothEnabled: topBar.bluetoothEnabled
isActive: topBar.shellRoot ? topBar.shellRoot.controlCenterVisible : false

View File

@@ -82,6 +82,7 @@ ShellRoot {
// Audio properties from AudioService
property int volumeLevel: AudioService.volumeLevel
property bool volumeMuted: AudioService.sinkMuted
property var audioSinks: AudioService.audioSinks
property string currentAudioSink: AudioService.currentAudioSink
@@ -320,6 +321,7 @@ ShellRoot {
networkStatus: root.networkStatus
wifiSignalStrength: root.wifiSignalStrength
volumeLevel: root.volumeLevel
volumeMuted: root.volumeMuted
bluetoothAvailable: root.bluetoothAvailable
bluetoothEnabled: root.bluetoothEnabled
shellRoot: root