A Spotify widget for the Omarchy bar. It stays in the bar whether or not Spotify is running, adds search and a new releases tab, and has a button for keeping the music playing with no window in the way.
omarchy plugin add https://github.com/Adam-loy/Spotify_Wheel.git --enableThat clones the repository into ~/.config/omarchy/plugins/, puts the widget in
the bar, and asks which section to use. To do it without the questions:
omarchy plugin add https://github.com/Adam-loy/Spotify_Wheel.git --enable --yesOr clone it yourself, if you would rather read the code first:
git clone https://github.com/Adam-loy/Spotify_Wheel.git \
~/.config/omarchy/plugins/io.github.adam-loy.spotify-wheel
omarchy restart shell
omarchy plugin enable io.github.adam-loy.spotify-wheel --section centerThe directory name does not matter to the shell, but the plugin id does: it
comes from "id" in manifest.json and is
io.github.adam-loy.spotify-wheel. Every command below takes that id, not the
repository name. omarchy plugin list shows the id the shell actually found, and
omarchy-shell shell rescanPlugins makes it look again after a fresh clone.
To put it somewhere else on the bar later:
omarchy plugin move io.github.adam-loy.spotify-wheel --section rightomarchy plugin remove io.github.adam-loy.spotify-wheelThat takes it off the bar, unloads it from the running shell, and deletes the
folder. Confirm the prompt, or pass --yes for a script. If you cloned it
yourself into a directory named something other than the id, remove that
directory yourself.
To keep the files and only take the widget off the bar, use disable instead:
omarchy plugin disable io.github.adam-loy.spotify-wheelTo update an existing install to the latest commit:
omarchy plugin update io.github.adam-loy.spotify-wheelNothing is written outside ~/.config/omarchy/plugins/, apart from artwork the
widget caches under ~/.cache/Spotify_Wheel/. Uninstalling removes the plugin
folder; the artwork cache is left alone and is safe to delete.
- Always in the bar. Upstream hides the slot when Spotify is closed. This
one keeps a dimmed
Spotifylabel and the icon there, so it is a way to start the app and search without hunting for it. The setting still exists if you would rather it got out of the way. - Search and new releases, in two extra tabs. Spotify's own Web API is not used at all — see Where the data comes from.
- Start / Show app, which runs Spotify with no window at all. See Background playback.
- Close app, which quits Spotify and stops the music.
Everything here works on a free Spotify account. Two things follow from that, and they shape the whole design.
The Spotify Web API is off limits. Since February 2026 it requires a Premium
app owner, and /browse/new-releases is gone. So there is no "current
playback", no "your library", and no official new releases feed. What the widget
shows on screen still comes from Spotify over MPRIS, which needs no account and
no key. Only discovery — search and new releases — has to come from elsewhere.
spotifyd is not used. It would be the proper way to run Spotify as a
headless player, but it needs Premium, and a free account cannot use it.
Because there is no API, a catalogue hit cannot be turned into a Spotify track
by ID. Clicking a result hands Spotify a spotify:search: URI for that title
and artist through MPRIS, which opens the search inside the app. It is a
handoff, not playback.
| Tab | Source | Notes |
|---|---|---|
| Now playing | Spotify, over MPRIS | No key, no account, no network |
| Search, tracks | Deezer | Public JSON, no auth |
| Search, albums | Deezer | Public JSON, no auth |
| New releases | Apple's search API | Public JSON, no auth |
Both catalogues are asked through a generated sh script rather than fetched
straight from QML. It allowlists the two hosts, caps the response at 2 MB,
refuses redirects and follows no cookies, and rejects a body that does not start
with { or [ — which is what a captive portal or a proxy error page looks
like, and which would otherwise turn into a parse error in the panel.
Apple's search supports sort=recent, but it means recent, among the things
that match — relevance still outranks date. Ask it for jazz and the top of
the page is a 1956 compilation also called Jazz. So the tab re-sorts the
answer newest-first, and puts the release date next to every row.
That is not cosmetic. Apple's free index is unevenly fresh, and the tab will
show you which: hip hop has records from this month at the top, jazz stops
around 2023. It is a good list of what Apple's index has, not a complete feed of
everything released. Deezer's editorial "new releases" endpoints, which would
have been a better source, now answer empty.
The point of the widget is that you can listen to Spotify without ever seeing Spotify. Start launches it and puts its window on a Hyprland special workspace, so playback begins and the window is somewhere you cannot land on by accident. Show app takes it back off and puts it on whatever workspace you are looking at, following it there.
The special workspace is special:spotify rather than the conventional
special:minimized, specifically so that toggling it cannot disturb a scratchpad
you have bound to something of your own.
This is not a daemon. On a free account there is no supported way to run
Spotify without its window: spotifyd needs Premium, as does Spotify's own
Soloist client. So the process is real, the window is just somewhere you are
not. If you want no process at all, that needs Premium.
Two details are worth knowing if you read Model.js:
- Hyprland 0.55 moved dispatchers behind a Lua layer, so this needs
hyprctl dispatch 'hl.dsp.window.move({ window = "address:0x…", workspace = "special:spotify", follow = false })'. The old string form,hyprctl dispatch movetoworkspace special:…, is accepted and does nothing on 0.56. Note thathyprctl dispatchwraps the expression inhl.dispatchfor you; wrapping it again is an error. - Because of exactly that, a successful exit code means nothing. The script re-reads the window's workspace afterwards and exits non-zero if it did not move, so a silent no-op cannot be reported to the panel as success.
startOnLogin does the same thing a few seconds after login. It is off by
default, because starting a music player unprompted is not a default anyone
would want.
Close app is the one control that stops the music. It asks MPRIS to quit, rather than killing the process: only the player knows whether it is willing to quit, and a kill mid-write would leave the next start slower.
That makes it deliberately unlike Start. Minimising — moving the window to
special:spotify — is the everyday way to get Spotify out of the way while it
keeps playing. This ends it.
The button is disabled when Spotify is not running, and when the player reports
canQuit as false, so it never looks like it worked when it did not.
Catalogue responses are not trusted with images. Every remote URL, the current
cover and every search result, goes through one probe that checks the host
against an allowlist of scdn.co, dzcdn.net, and mzstatic.com, then
downloads it to a local file with ImageMagick. The panel only ever loads a
file:// path that probe wrote, so a hostile or lookalike host in a catalogue
body cannot reach the image loader — evilscdn.co and dzcdn.net.evil.com
are both refused.
In the bar's settings panel, under Spotify:
| Setting | Default | |
|---|---|---|
| Hide the widget when Spotify is not running | off | Turn on to hide the slot entirely |
| Search shows | Tracks | Or Albums |
| New releases starts on | pop | The genre the New tab asks for |
| Maximum label width | 200 | Wider titles scroll |
| Show artist | on | |
| Show progress | on | Underline in the bar |
| Left click | Open panel | The other button becomes play/pause |
| Mouse wheel | Previous/next track | Or volume |
| Accent color | Spotify green | Or album art, theme accent, bar foreground |
| Album cover behind the panel | on | |
| How much cover shows through | 55 |
For keybindings and scripts. -- passes an argument.
omarchy-shell Spotify_Wheel toggle
omarchy-shell Spotify_Wheel playPause
omarchy-shell Spotify_Wheel next
omarchy-shell Spotify_Wheel previous
omarchy-shell Spotify_Wheel launch # start, and hide the window
omarchy-shell Spotify_Wheel show # bring the window back
omarchy-shell Spotify_Wheel minimize # hide an already-open window
omarchy-shell Spotify_Wheel quit # quit Spotify, stopping playback
omarchy-shell Spotify_Wheel search 'daft punk'
omarchy-shell Spotify_Wheel releases 'hip hop'
omarchy-shell Spotify_Wheel statusstatus and searchDebug return JSON for debugging.
curl— the only hard one; search and releases do nothing without it.ImageMagick(magick) — album art and the cover backdrop. Without it the panel keeps its plain background and rows show no thumbnails.jq— the background-playback buttons.hyprctl— the background-playback buttons, on Hyprland 0.55 or newer for the Lua dispatcher form.
node test/qml-test.js # Panel.qml structure -- see below
node test/model-test.js # MPRIS helpers, artwork probe, cache parsing
node test/search-test.js # URLs, both parsers, host vetting, live catalogues
node test/ops-test.js # background playback, cache clearing on a fake profile
sh test/probe-test.sh # the artwork gate, against hostile URLs
sh test/run.sh # all of the aboveqml-test.js is not decoration. qmllint passes a Panel.qml that Quickshell
refuses to load -- it reported nothing at all on one with a stray brace, and the
only clue was the shell's log. The structure test is what catches that, so run
it first; test/run.sh does.
search-test.js makes live requests to both catalogues; it fails if either is
unreachable. The QML side is checked with qmllint, and qml/harness.qml runs
the two logic files under Quickshell's own JavaScript engine, which is not
Node's:
qml/harness.qml # symlink Model.js and Search.js beside it, then:
QML_IMPORT_PATH=/usr/share/omarchy/shell:/usr/lib/qt6/qml qs -p harness.qmlThat harness has already earned its place: it is what caught StdioCollector
handlers that read a text argument the signal does not pass, which Node never
noticed and which would have left search and thumbnails silently broken in the
bar.
CC0 1.0 Universal — see LICENSE. Do what you want with it.
External dependencies are listed under Requirements; none of them are bundled, and all are packaged for Arch and Omarchy.
