Ghost Assembly QuickMusic

GNOME Shell extension

QuickMusic

What is playing, in Quick Settings and the top bar.

Shows the current track from any MPRIS player — Spotify (including the Flatpak), browsers, Rhythmbox, VLC and the rest — with play/pause, previous, next and a button to bring the player to the front.

  • GNOME Shell 50
  • Any MPRIS player
  • GPL-3.0-or-later

Install Source on GitHub

On this page
  1. 01Overview
  2. 02Install
  3. 03Players
  4. 04Preferences
  5. 05Keyboard
  6. 06Architecture
  7. 07Testing
  8. 08Packaging
  9. 09Releasing
  10. 10Development

01

Overview

QuickMusic adds one tile to Quick Settings and, while something plays or is paused, one item to the top bar. It reads the standard MPRIS interface players already publish on the session bus, so there is nothing else to install.

Quick Settings tile

Title and artist; click to play or pause. The arrow opens cover art, album, previous / play-pause / next, “Open ‹player›” and a player picker.

Top bar item

“Artist – Title” while something is playing or paused, hidden otherwise. Left-click plays and pauses; right-click opens the same controls.

Follows the active player

Whichever started playing most recently, and the last one used once nothing is. Pin one from the picker to stay on it.

Any MPRIS player

Spotify (including the Flatpak), Firefox, Chromium, Rhythmbox, VLC and most others. Nothing extra to install.

02

Install

Requires

  • GNOME Shell 50.
  • A player that speaks MPRIS. Most do. A Flatpak player also needs its manifest to allow org.mpris.MediaPlayer2.<name> on the session bus; Spotify’s does.
  • GVfs for cover art served over https. It is part of a standard GNOME install, and the Shell’s own media controls rely on it too.

From a release

Shell
$ curl -LO https://github.com/Ghost-Assembly/quickmusic/releases/latest/download/[email protected]
$ gnome-extensions install --force [email protected]
$ gnome-extensions enable [email protected]

From a clone

Shell
$ just setup
$ just install
$ just enable

Log out and back in. A newly installed extension is only picked up when the Shell next starts. On Wayland that means a new session.

03

Players

QuickMusic speaks MPRIS, the D-Bus interface desktop media players publish. With several running, the tile shows the first that applies:

  1. The pinned player, if it is running.
  2. The playing player that started most recently.
  3. The last player shown, so pausing does not jump elsewhere.
  4. The first by name.

A pin is stored without any instance suffix — chromium, not chromium.instance2 — so it survives the player restarting. A pinned player that is not running stays in the picker, marked “(not running)”, so the pin can be seen and undone.

just mpris-check
$ just mpris-check
  chromium (Chrome) Stopped
    (no track)
    can:   (nothing)
* spotify (Spotify) Playing
    Foo Fighters – My Hero
    album: The Colour And The Shape
    can:   canPlay canPause canGoPrevious canGoNext canRaise

04

Preferences

Open them from the Extensions app, or with just prefs from a clone.

Setting Default Note
Show in the top bar On Hidden while nothing is playing or paused, either way.
Top bar label width 40 characters 10 to 120. Longer text is cut with an ellipsis.
Pinned player None Set from the Quick Settings picker; “Forget” clears it.

05

Keyboard

The top bar item works without a mouse once it has focus.

Input Does
Left-click Play or pause
Right-click Open the controls
Enter or Space Play or pause
Menu or Shift+F10 Open the controls

A screen reader hears the whole track and what activating the item would do — “Pause Foo Fighters – My Hero” — even when the visible label is cut short.

06

Architecture

extension.js is deliberately thin: it builds the MPRIS watcher and the panel on enable, and tears both down on disable. Every decision lives in one pure module; the files around it only move data between it and the bus or the Shell, so the suite can test the decisions on plain Node.

settings.jsSchema keys and their wording, shared by panel.js and prefs.js and checked against the gschema.

Players are untrusted. Text is set as plain text, flattened to one line and capped; cover art loads only from https:// or file://. See the security policy.

07

Testing

The unit suite runs on Node with the Shell’s modules replaced by recording stubs. just test-live adds what CI cannot have: it boots a headless GNOME Shell with a fake player on a private bus, and checks that the tile picks up a new player, follows a pause, drops a player that quits, and survives a disable and re-enable with no JavaScript error. It then checks the shell log for two kinds of leak: signal or object lifetime warnings, and a GSource that outlived the disable (a Source ID … was not found or GSource … still active line). It then runs the packaging check below. just test-docs holds this site to its rules in Chromium and Firefox: accessibility in both color schemes, no JavaScript, no other origin, and no sideways scrolling on a phone.

prefs.js has no coverage, by design: it holds only Adw and Gtk widget construction, and the key list and wording it shows live in modules/settings.js, tested on plain Node. The exclusion is written in both vitest.config.js and sonar-project.properties so the two agree. modules/mpris.js’s race logic — a player connecting, quitting or being dropped mid-load — is under test with a fake bus. The wire format itself is still checked against the real session bus, by just mpris-check, and against scripts/fake-player.js by just test-live.

08

Packaging

An extension ships as a plain zip, conventionally named <uuid>.shell-extension.zip, with metadata.json at the archive root. just build makes it with zip rather than GNOME’s gnome-extensions pack, because that tool comes with the gnome-shell package and CI has no GNOME.

The official packer stays the authority all the same: just pack-check runs it and fails on any difference from the built zip, so the two cannot drift.

Every shipped icon is decoded, not just packed. An icon that fails to load is silent — the Shell draws nothing and logs nothing. A comment placed before the <svg> element is enough to defeat gdk-pixbuf’s format sniffing, so the pack check loads each icon and fails if it does not decode to something visible.

09

Releasing

Set version-name in metadata.json and version in package.json, commit, then tag and push. Run just test-live first: neither the headless check nor the packer check runs in CI.

Shell
$ git tag -a v0.1.0 -m 'release v0.1.0'
$ git push origin v0.1.0

The release workflow checks the tag against both files and stops before building if they disagree — a release whose zip tells GNOME it is another version is worse than none. It then runs just ci and attaches the zip to a GitHub release.

Uploading to extensions.gnome.org is deliberately left out of CI. gnome-extensions upload authenticates with the account password rather than a scoped token, and every upload goes to human review anyway.

10

Development

justfile
just              # list every recipe
just test         # unit suite
just test-docs    # this site, in Chromium and Firefox
just lint         # eslint, prettier, gschema, shellcheck
just ci           # what CI runs: lint, test, test-docs, security, build
just test-live    # headless gnome-shell, bundle and live-bus checks
just mpris-check  # list the players on this session bus
just fake-player  # put a test player on the bus
just docs         # serve this site on localhost:8000