Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions docs/IPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,94 @@ dms ipc call mpris playPause
dms ipc call mpris next
```

## Target: `media`

Custom media source control for non-MPRIS media players. This allows external tools (like rmpc, cmus, mpv) to push media metadata to DMS and control playback through shell commands.

The custom source integrates seamlessly with MPRIS players - DMS automatically selects the active source based on which one has content loaded. Custom source takes precedence when it has a track title set.

### Functions

**`update <jsonData>`**
- Update the current media state with JSON data
- Parameters: `jsonData` - JSON string with media information
- Supported JSON fields:
- `title` - Track title
- `artist` - Artist name
- `album` - Album name
- `artUrl` - URL or path to album art
- `state` - Playback state (0=stopped, 1=playing, 2=paused)
- `position` / `elapsed` - Current position in seconds
- `length` / `duration` - Total length in seconds
- `volume` - Volume level (0.0-1.0)
- `available` - Whether the source is available (true/false)
- `identity` - Player identity/name
- `sourceId` - Unique source identifier
- Returns: "MEDIA_UPDATE_SUCCESS" or error message

**`setCommands <jsonData>`**
- Set shell commands to execute for playback control
- Parameters: `jsonData` - JSON string with command mappings
- Supported commands:
- `play` - Command to start playback
- `pause` - Command to pause playback
- `toggle` - Command to toggle play/pause
- `next` - Command to skip to next track
- `previous` / `prev` - Command to go to previous track
- Returns: "MEDIA_COMMANDS_SET_SUCCESS" or error message

**`clear`**
- Clear the current media state
- Returns: "MEDIA_CLEAR_SUCCESS"

**`status`**
- Get current custom media source status
- Returns: JSON object with current media state

**`play`**
- Execute the configured play command

**`pause`**
- Execute the configured pause command

**`playPause`**
- Execute the configured toggle command

**`next`**
- Execute the configured next track command

**`previous`**
- Execute the configured previous track command

### Examples

**Setting up rmpc integration with hooks (recommended):**

1. Create a hook script (`~/.config/rmpc/dms_hook`):
```bash
#!/bin/bash
# Set playback commands (idempotent - safe to call every time)
dms ipc call media setCommands '{"play":"rmpc play","pause":"rmpc pause","toggle":"rmpc togglepause","next":"rmpc next","prev":"rmpc prev"}'

# Push current track info to DMS
dms ipc call media update "{\"title\":\"$TITLE\",\"artist\":\"$ARTIST\",\"album\":\"$ALBUM\",\"duration\":$DURATION,\"state\":1,\"available\":true,\"sourceId\":\"rmpc\",\"identity\":\"RMPC\"}"
```

2. Make it executable and add to rmpc's `on_song_change` hook in `~/.config/rmpc/config.ron`:
```ron
on_song_change: [
"~/.config/rmpc/dms_hook"
]
```

The hook sets playback commands on every call (idempotent) and pushes track updates. This ensures commands are always available even after DMS restarts.

**Query current status:**
```bash
dms ipc call media status
# Returns: {"available":true,"sourceId":"rmpc","title":"Song","artist":"Artist",...}
```

## Target: `lock`

Screen lock control and status.
Expand Down
62 changes: 62 additions & 0 deletions quickshell/DMSShellIPC.qml
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,68 @@ Item {
target: "mpris"
}

IpcHandler {
function update(jsonData: string): string {
try {
const data = JSON.parse(jsonData)
CustomMediaSource.update(data)
return "MEDIA_UPDATE_SUCCESS"
} catch (e) {
return "MEDIA_UPDATE_ERROR: " + e.message
}
}

function setCommands(jsonData: string): string {
try {
const cmds = JSON.parse(jsonData)
CustomMediaSource.setCommands(cmds)
return "MEDIA_COMMANDS_SET_SUCCESS"
} catch (e) {
return "MEDIA_COMMANDS_ERROR: " + e.message
}
}

function clear(): string {
CustomMediaSource.clear()
return "MEDIA_CLEAR_SUCCESS"
}

function status(): string {
return JSON.stringify({
available: CustomMediaSource.available,
sourceId: CustomMediaSource.sourceId,
title: CustomMediaSource.trackTitle,
artist: CustomMediaSource.trackArtist,
album: CustomMediaSource.trackAlbum,
state: CustomMediaSource.playbackState,
position: CustomMediaSource.position,
length: CustomMediaSource.length
})
}

function play(): void {
CustomMediaSource.play()
}

function pause(): void {
CustomMediaSource.pause()
}

function playPause(): void {
CustomMediaSource.togglePlaying()
}

function next(): void {
CustomMediaSource.next()
}

function previous(): void {
CustomMediaSource.previous()
}

target: "media"
}

IpcHandler {
function toggle(provider: string): string {
if (!provider)
Expand Down
2 changes: 1 addition & 1 deletion quickshell/Modules/DankBar/WidgetHost.qml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Loader {

readonly property bool orientationMatches: (axis?.isVertical ?? false) === isInColumn

active: orientationMatches && getWidgetVisible(widgetId, DgopService.dgopAvailable) && (widgetId !== "music" || MprisController.activePlayer !== null)
active: orientationMatches && getWidgetVisible(widgetId, DgopService.dgopAvailable) && (widgetId !== "music" || MprisController.currentPlayer !== null)
sourceComponent: getWidgetComponent(widgetId, components)
opacity: getWidgetEnabled(widgetData?.enabled) ? 1 : 0

Expand Down
4 changes: 2 additions & 2 deletions quickshell/Modules/DankBar/Widgets/AudioVisualization.qml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import qs.Services
Item {
id: root

readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property var activePlayer: MprisController.currentPlayer
readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
readonly property bool isPlaying: MprisController.currentIsPlaying

width: 20
height: Theme.iconSize
Expand Down
14 changes: 7 additions & 7 deletions quickshell/Modules/DankBar/Widgets/Media.qml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import qs.Widgets
BasePill {
id: root

readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property var activePlayer: MprisController.currentPlayer
readonly property bool playerAvailable: activePlayer !== null
readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity)
Expand Down Expand Up @@ -191,15 +191,15 @@ BasePill {
height: 24
radius: 12
anchors.horizontalCenter: parent.horizontalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
color: MprisController.currentIsPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3

DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
name: MprisController.currentIsPlaying ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
color: MprisController.currentIsPlaying ? Theme.background : Theme.primary
}

MouseArea {
Expand Down Expand Up @@ -381,15 +381,15 @@ BasePill {
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
color: MprisController.currentIsPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3

DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
name: MprisController.currentIsPlaying ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
color: MprisController.currentIsPlaying ? Theme.background : Theme.primary
}

MouseArea {
Expand Down
17 changes: 10 additions & 7 deletions quickshell/Modules/DankDash/DankDashPopout.qml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ DankPopout {
property bool __dropdownRightEdge: false
property var __dropdownPlayer: null
property var __dropdownPlayers: []
property bool __dropdownPlayerIsCustom: false

function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
Expand All @@ -43,11 +44,12 @@ DankPopout {
__dropdownType = 2;
}

function __showPlayersDropdown(pos, rightEdge, player, players) {
function __showPlayersDropdown(pos, rightEdge, player, players, isCustom) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownPlayerIsCustom = isCustom || false;
__dropdownType = 3;
}

Expand Down Expand Up @@ -82,6 +84,7 @@ DankPopout {
isRightEdge: root.__dropdownRightEdge
activePlayer: root.__dropdownPlayer
allPlayers: root.__dropdownPlayers
activeIsCustom: root.__dropdownPlayerIsCustom
onCloseRequested: root.__hideDropdowns()
onPanelEntered: root.__stopCloseTimer()
onPanelExited: root.__startCloseTimer()
Expand All @@ -95,12 +98,12 @@ DankPopout {
AudioService.sink.audio.volume = volume;
}
}
onPlayerSelected: player => {
const currentPlayer = MprisController.activePlayer;
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
onPlayerSelected: source => {
const currentPlayer = MprisController.currentPlayer;
if (currentPlayer && !source?.isCustom && currentPlayer !== source?.player && currentPlayer.canPause) {
currentPlayer.pause();
}
MprisController.activePlayer = player;
MprisController.selectSource(source);
root.__hideDropdowns();
}
onDeviceSelected: device => {
Expand Down Expand Up @@ -382,8 +385,8 @@ DankPopout {
onShowAudioDevicesDropdown: (pos, screen, rightEdge) => {
root.__showAudioDevicesDropdown(pos, rightEdge);
}
onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => {
root.__showPlayersDropdown(pos, rightEdge, player, players);
onShowPlayersDropdown: (pos, screen, rightEdge, player, players, isCustom) => {
root.__showPlayersDropdown(pos, rightEdge, player, players, isCustom);
}
onHideDropdowns: root.__hideDropdowns()
onVolumeButtonExited: root.__startCloseTimer()
Expand Down
32 changes: 15 additions & 17 deletions quickshell/Modules/DankDash/MediaDropdownOverlay.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Item {
property int dropdownType: 0
property var activePlayer: null
property var allPlayers: []
property bool activeIsCustom: false // True if activePlayer is CustomMediaSource
property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false

Expand Down Expand Up @@ -407,12 +408,19 @@ Item {
required property var modelData
required property int index

readonly property bool isActive: {
if (modelData?.isCustom) {
return activeIsCustom
}
return modelData?.player === activePlayer
}

width: parent.width
height: 48
radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1
border.color: isActive ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: isActive ? 2 : 1

Row {
anchors.left: parent.left
Expand All @@ -422,9 +430,9 @@ Item {
width: parent.width - Theme.spacingM * 2

DankIcon {
name: "music_note"
name: modelData?.isCustom ? "queue_music" : "music_note"
size: 20
color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}

Expand All @@ -436,28 +444,18 @@ Item {
text: {
if (!modelData)
return "Unknown Player";
const identity = modelData.identity || "Unknown Player";
const trackTitle = modelData.trackTitle || "";
return trackTitle.length > 0 ? identity + " - " + trackTitle : identity;
return modelData.identity || "Unknown Player";
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
font.weight: isActive ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}

StyledText {
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lose the artist data here yea?

text: isActive ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
Expand Down
Loading
Loading