1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-03 03:59:11 -04:00

Compare commits

...

9 Commits

Author SHA1 Message Date
purian23 943ffb432c fix(battery): reliably apply configured power profiles at shell startup
Port 1.5

(cherry picked from commit 400a18a8ed)
2026-08-02 23:02:00 +00:00
purian23 7063448b80 refactor(media OSD): add previous/next UI controls
Port 1.5

(cherry picked from commit 32ddf614c3)
2026-08-02 22:10:33 +00:00
purian23 52afbc9801 refactor(mpris): consolidate equivalent-player resolution & update track matching
Port 1.5

(cherry picked from commit 19d919ed5c)
2026-08-02 22:09:37 +00:00
Rubén García 6edb985847 refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players

* fix(mpris): address review feedback on source stealing and title stripping

- _bestPlayingPlayer: when the active player is playing but not
  controllable, only hand ownership to a controllable peer that passes
  isSameTrack; unrelated players can no longer steal the active source.
- displayTrackTitle: keep the full trackTitle for display. Suffix
  stripping now only applies to the isSameTrack matching key, and only
  for a known app-name allowlist (YouTube, SoundCloud, browsers, etc.)
  so legitimate titles containing ' | ' are never mangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Port 1.5

(cherry picked from commit 594a2cde19)
2026-08-02 21:34:49 +00:00
bbedward 41efe6ad15 tailscale: add missing ID
related #2969
port 1.5

(cherry picked from commit ef191babb7)
2026-07-31 13:37:43 +00:00
bbedward 4fb6a17e1b launcher: fix regex escaping launch args
fixes #2961
port 1.5

(cherry picked from commit a710d6d7cc)
2026-07-30 18:35:04 +00:00
bbedward e771e3b675 greeter: lua for hypr fallback 2026-07-30 14:31:39 -04:00
bbedward b1b7aa7aa2 Revert "dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926)"
This reverts commit b8e2ce1da8.
2026-07-30 14:30:51 -04:00
bbedward c128793239 qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5

(cherry picked from commit 64461c534f)
2026-07-27 17:41:17 +00:00
10 changed files with 374 additions and 108 deletions
@@ -469,6 +469,8 @@ PluginComponent {
}
MouseArea {
id: peerMouseArea
z: -1
anchors.fill: parent
hoverEnabled: true
@@ -490,7 +490,7 @@ Item {
}
StyledText {
text: activePlayer?.trackAlbum || ""
text: MprisController.stableAlbum
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary
width: parent.width
+2 -4
View File
@@ -479,8 +479,6 @@ Item {
delegate: Item {
id: delegateItem
required property var modelData
required property int index
property var dockButton: {
switch (itemData.type) {
@@ -601,7 +599,7 @@ Item {
height: delegateItem.height
actualIconSize: root.iconSize
dockApps: root
index: delegateItem.index
index: model.index
}
DockTrashButton {
@@ -626,7 +624,7 @@ Item {
appData: itemData
contextMenu: root.contextMenu
dockApps: root
index: delegateItem.index
index: model.index
parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: {
+12 -8
View File
@@ -386,16 +386,20 @@ end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1
TEMP_CONFIG=$(mktemp --suffix=.lua)
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
hl.env("DMS_RUN_GREETER", "1")
misc {
disable_hyprland_logo = true
}
hl.config({
misc = {
disable_hyprland_logo = true,
},
})
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
hl.on("hyprland.start", function()
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
+89 -23
View File
@@ -156,9 +156,10 @@ DankOSD {
if (MprisController.isFirefoxYoutubeHoverPreview(player))
return;
const newTitle = player.trackTitle || "";
const newArtist = player.trackArtist || "";
const newAlbum = player.trackAlbum || "";
const metaPlayer = MprisController.bestMetadataPlayer(player);
const newTitle = MprisController.displayTrackTitle(metaPlayer);
const newArtist = metaPlayer.trackArtist || "";
const newAlbum = metaPlayer.trackAlbum || "";
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
root._displayTitle = newTitle;
@@ -263,37 +264,102 @@ DankOSD {
}
}
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
Row {
id: transportControls
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
DankIcon {
anchors.centerIn: parent
name: root._displayIcon
size: Theme.iconSize
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: Theme.iconSize - 10
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: prevButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoPrevious ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.previousOrRewind();
root.hide();
}
}
}
MouseArea {
id: playPauseButton
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
DankIcon {
anchors.centerIn: parent
name: root._displayIcon
size: Theme.iconSize
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: playPauseButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
}
}
}
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: Theme.iconSize - 10
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoNext ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.next();
root.hide();
}
}
}
}
Column {
x: parent.gap * 2 + Theme.iconSize
width: parent.width - Theme.iconSize - parent.gap * 3
x: parent.gap * 2 + transportControls.width
width: parent.width - transportControls.width - parent.gap * 3
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
+8 -1
View File
@@ -23,6 +23,13 @@ Singleton {
}
}
Connections {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onHasPerformanceProfileChanged() {
root.applyPowerProfile();
}
}
function applyPowerProfile() {
if (!batteryAvailable)
return;
@@ -32,7 +39,7 @@ Singleton {
const targetProfile = parseInt(profileValue);
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
return;
PowerProfiles.profile = targetProfile;
PowerProfileWatcher.applyProfile(targetProfile);
}
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
+30 -53
View File
@@ -11,7 +11,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("DMSService")
property bool dmsAvailable: false
readonly property bool dmsAvailable: isConnected
property var capabilities: []
property int apiVersion: 0
property string cliVersion: ""
@@ -21,7 +21,7 @@ Singleton {
property var availableThemes: []
property var installedThemes: []
property bool isConnected: false
property bool isConnecting: false
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -72,9 +72,10 @@ Singleton {
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
detectUpdateCommand();
}
if (!socketPath || socketPath.length === 0)
return;
detectUpdateCommand();
requestSocket.connected = true;
}
function detectUpdateCommand() {
@@ -82,12 +83,6 @@ Singleton {
checkAurHelper.running = true;
}
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true;
}
}
Process {
id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"]
@@ -105,7 +100,6 @@ Singleton {
} else {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -114,7 +108,6 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -135,7 +128,6 @@ Singleton {
updateCommand = "dms update";
}
checkingUpdateCommand = false;
startSocketConnection();
}
}
@@ -143,52 +135,27 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
if (exitCode === 0) {
root.dmsAvailable = true;
connectSocket();
} else {
root.dmsAvailable = false;
}
}
}
function connectSocket() {
if (!dmsAvailable || isConnected || isConnecting) {
return;
}
isConnecting = true;
requestSocket.connected = true;
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (connected) {
if (linkUp) {
root.isConnected = true;
root.isConnecting = false;
root.connectionStateChanged();
subscribeSocket.connected = true;
} else {
root.isConnected = false;
root.isConnecting = false;
root.apiVersion = 0;
root.capabilities = [];
root.connectionStateChanged();
return;
}
root.isConnected = false;
root.apiVersion = 0;
root.capabilities = [];
root.failPendingRequests();
root.connectionStateChanged();
}
parser: SplitParser {
@@ -219,10 +186,10 @@ Singleton {
connected: false
onConnectionStateChanged: {
root.subscribeConnected = connected;
if (connected) {
sendSubscribeRequest();
}
root.subscribeConnected = linkUp;
if (!linkUp)
return;
sendSubscribeRequest();
}
parser: SplitParser {
@@ -446,10 +413,20 @@ Singleton {
function handleResponse(response) {
const callback = pendingRequests[response.id];
if (!callback)
return;
delete pendingRequests[response.id];
callback(response);
}
if (callback) {
delete pendingRequests[response.id];
callback(response);
function failPendingRequests() {
const pending = pendingRequests;
pendingRequests = {};
clipboardRequestIds = {};
for (const id in pending) {
pending[id]({
"error": "not connected to DMS socket"
});
}
}
+138 -8
View File
@@ -47,6 +47,7 @@ Singleton {
// Chromium can report blank metadata between tracks
property string stableTitle: ""
property string stableArtist: ""
property string stableAlbum: ""
Connections {
target: root.activePlayer
@@ -59,6 +60,9 @@ Singleton {
root._syncStableMeta();
root._checkIdle();
}
function onTrackAlbumChanged() {
root._syncStableMeta();
}
function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length;
@@ -72,8 +76,10 @@ Singleton {
onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
stableTitle = activePlayer?.trackTitle || "";
stableArtist = activePlayer?.trackArtist || "";
stableTitle = "";
stableArtist = "";
stableAlbum = "";
_syncStableMeta();
_checkIdle();
}
@@ -82,14 +88,24 @@ Singleton {
if (!p) {
stableTitle = "";
stableArtist = "";
stableAlbum = "";
return;
}
if (isFirefoxYoutubeHoverPreview(p))
return;
if (p.trackTitle)
stableTitle = p.trackTitle;
if (p.trackArtist)
stableArtist = p.trackArtist;
const metadataPlayer = bestMetadataPlayer(p);
const nextTitle = displayTrackTitle(metadataPlayer);
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
if (trackChanged) {
stableArtist = "";
stableAlbum = "";
}
if (nextTitle)
stableTitle = nextTitle;
if (metadataPlayer.trackArtist)
stableArtist = metadataPlayer.trackArtist;
if (metadataPlayer.trackAlbum)
stableAlbum = metadataPlayer.trackAlbum;
}
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
@@ -101,6 +117,7 @@ Singleton {
return;
root.stableTitle = "";
root.stableArtist = "";
root.stableAlbum = "";
root._resolveActivePlayer();
}
}
@@ -122,9 +139,30 @@ Singleton {
delegate: Connections {
required property MprisPlayer modelData
target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() {
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackTitleChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackArtistChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackAlbumChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onMetadataChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
}
}
@@ -133,9 +171,101 @@ Singleton {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
}
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
function _stripAppTitleSuffix(title: string): string {
const idx = title.lastIndexOf(" | ");
if (idx <= 0)
return title;
const suffix = title.substring(idx + 3).trim().toLowerCase();
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
}
function normalizedTrackTitle(player: MprisPlayer): string {
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
}
function displayTrackTitle(player: MprisPlayer): string {
return (player?.trackTitle || "").trim();
}
function normalizedTrackArtist(player: MprisPlayer): string {
return (player?.trackArtist || "").trim().toLowerCase();
}
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
const firstTitle = normalizedTrackTitle(first);
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
return false;
const firstArtist = normalizedTrackArtist(first);
const secondArtist = normalizedTrackArtist(second);
if (firstArtist && secondArtist)
return firstArtist === secondArtist;
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
if (firstUrl && secondUrl)
return firstUrl === secondUrl;
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
if (firstAlbum && secondAlbum)
return firstAlbum === secondAlbum;
return true;
}
function metadataQuality(player: MprisPlayer): int {
if (!player)
return -1;
let quality = player.trackArtist ? 100 : 0;
quality += player.trackTitle ? 40 : 0;
quality += player.trackAlbum ? 20 : 0;
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
quality += player.metadata?.["xesam:url"] ? 5 : 0;
return quality;
}
function equivalentPlayers(player: MprisPlayer): var {
if (!player)
return [];
return availablePlayers.filter(candidate => {
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
});
}
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
const equivalents = equivalentPlayers(player);
if (equivalents.length === 0)
return player;
return equivalents.reduce((best, candidate) => {
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
}, player);
}
function _bestPlayingPlayer(): MprisPlayer {
const playing = availablePlayers.filter(player => player.isPlaying);
if (playing.length === 0)
return null;
const controllable = playing.filter(player => player.canControl);
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
return activePlayer;
// Playing but not controllable: only a same-track controllable peer may take over
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
return mirror || activePlayer;
}
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
if (onlyEquivalentMirrors)
return null;
}
return controllable[0] || playing[0];
}
function _resolveActivePlayer(): void {
// A playing player always wins; otherwise keep the selection stable w/idle
const playing = availablePlayers.find(p => p.isPlaying);
const playing = _bestPlayingPlayer();
if (playing) {
if (activePlayer !== playing) {
activePlayer = playing;
+59 -2
View File
@@ -217,6 +217,64 @@ Singleton {
return envObj;
}
function splitShellArgs(str) {
const args = [];
let current = "";
let hasToken = false;
let quote = "";
let escaped = false;
for (const ch of str) {
if (escaped) {
current += ch;
escaped = false;
continue;
}
switch (quote) {
case "'":
if (ch === "'") {
quote = "";
continue;
}
current += ch;
continue;
case "\"":
switch (ch) {
case "\"":
quote = "";
continue;
case "\\":
escaped = true;
continue;
}
current += ch;
continue;
}
switch (ch) {
case "\\":
escaped = true;
continue;
case "'":
case "\"":
quote = ch;
hasToken = true;
continue;
case " ":
case "\t":
case "\n":
if (!hasToken && current.length === 0)
continue;
args.push(current);
current = "";
hasToken = false;
continue;
}
current += ch;
}
if (current.length > 0 || hasToken)
args.push(current);
return args;
}
function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command)
return;
@@ -230,8 +288,7 @@ Singleton {
cmd = [nvidiaCommand].concat(cmd);
if (override?.extraFlags) {
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
cmd = cmd.concat(extraArgs);
cmd = cmd.concat(splitShellArgs(override.extraFlags));
}
const userPrefix = SettingsData.launchPrefix?.trim() || "";
+33 -8
View File
@@ -26,7 +26,7 @@ Singleton {
return hash.toString(16).padStart(8, '0');
}
function getArtworkUrl(player) {
function _directArtworkUrl(player) {
if (!player) return "";
let artUrl = player.trackArtUrl || "";
@@ -54,6 +54,17 @@ Singleton {
return "";
}
function getArtworkUrl(player) {
const directUrl = _directArtworkUrl(player);
if (directUrl !== "")
return directUrl;
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
return candidate !== player && _directArtworkUrl(candidate) !== "";
});
return _directArtworkUrl(equivalent);
}
function _commit(u, artKey, srcUrl) {
resolvedArtUrl = u;
_committedArtKey = u !== "" ? artKey : "";
@@ -171,11 +182,25 @@ Singleton {
onActivePlayerChanged: _updateArtUrl()
Connections {
target: root.activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
target: MprisController
function onAvailablePlayersChanged() {
root._updateArtUrl();
}
}
Instantiator {
model: MprisController.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { root._updateArtUrl(); }
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtistChanged() { root._updateArtUrl(); }
function onTrackAlbumChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
}
}
function _trackKey() {
@@ -201,8 +226,8 @@ Singleton {
}
_pendingArtKey = key;
const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates.
if (key !== "" && key === _committedArtKey)
// Ignore duplicate notifications, but let a richer peer replace same-track art
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
return;
if (key !== "" && url !== "" && url === _committedSrcUrl) {
// Chrome can publish track metadata before its new artwork URL.