Repository navigation
Linux: the Spotify pill over MPRIS, and Mochi dances to it - #372
Merged
Merged
Conversation
On Linux "Open terminal" always opened the session's folder in VS Code. The Windows build already finds the window a Claude Code session runs in by walking up the process tree from the relay; this does the same on Linux and raises the window with whatever the desktop allows. Finding it: - pipe.rs: the Unix socket's SO_PEERCRED gives the relay's PID (tokio's peer_cred), as GetNamedPipeClientProcessId does on Windows. - session_window.rs: parse_stat reads /proc/<pid>/stat (the name ends at the last ')', so names with spaces and parentheses are read right), a Linux TREE_TOPS list (systemd, init, logins, sshd, display and session managers, compositors and panels that own windows) ends the walk, and PID 1 and processes of another user are left out of the table. A session now keeps a list of candidate PIDs: the window owner on Windows, every ancestor on Linux, where which one owns a window is asked only on click. - choose_window: the windows of the nearest ancestor that has any, the one titled after the session folder first. Raising it (platform/linux_focus.rs), off the UI thread, best way first: - KDE Plasma on Wayland: a temporary KWin script loaded over D-Bus from the private runtime dir activates the window by PID (Plasma 6 windowList / activeWindow, Plasma 5 clientList / activeClient), calls us back with Activated or NotFound, and is then unloaded and its file removed. - X11 (and XWayland windows): EWMH through x11rb, _NET_CLIENT_LIST + _NET_WM_PID, then _NET_CURRENT_DESKTOP and _NET_ACTIVE_WINDOW (source 2). - kitty: kitten @ focus-window --match pid:<shell> selects the tab when its remote control listens on a socket. - Nothing worked (GNOME Wayland native windows, tmux, ssh): VS Code, as before. Before use the remembered chain is re-read from /proc and cut where it no longer links, so a closed terminal's reused PID is never raised. Only numbers from /proc reach the script and the commands (the folder name travels as character codes); commands get separate arguments, never a shell. dbus and x11rb were already in Cargo.lock (keyring, global-hotkey) with the same features: nothing new to download or build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJm3SRL9k3gBPaVtfPhwcT
The Mac has a Spotify pill (#246) and Mochi dances while music plays; the Windows and Linux app had neither. Linux has what the Mac gets from Spotify's notification and AppleScript in MPRIS, Spotify's D-Bus interface, so the pill comes to Linux first. Rust (src-tauri/src/spotify.rs, Linux only): - A small MPRIS client on libdbus (`dbus` 0.9.12, already in Cargo.lock through keyring: nothing new is downloaded). While the pill is declared, one thread blocks on the session bus and wakes only for Spotify's PropertiesChanged and Seeked signals and NameOwnerChanged (Spotify starting or quitting): no polling, 0 % CPU when nothing happens. Undeclaring the pill pings that thread off the bus. - Track (id, title, artist, album, length, cover URL), status, position, shuffle, loop status and volume, cleaned like the Mac (shortTitle, shortArtist). Position isn't signalled: it is read when the status or the track changes and when the card shows, and the page runs it on from a timestamp, as SpotifyController.position(at:). - Controls: PlayPause, Next, Previous, SetPosition (Seek on older builds), Shuffle, LoopStatus, Volume. Open raises Spotify, else starts `spotify` from PATH, the Flatpak or the Snap, else opens the download page. - Covers: the CSP keeps remote images out of the page, so Rust fetches them (Spotify's CDN only, or a local track's file), checks the bytes are an image and hands a data URL over; nothing is fetched while paused. Page: - `integration_spotify` in the catalog (Mac values), with a new `linux` support next to `windows`: offered on Linux only, nothing to set up; declared from Settings → Integrations. - SpotifyPill and SpotifyCardView ported: dancing mini Mochi with play/pause and next on hover; cover, title, artist · album, progress with seek, shuffle / previous / play-pause / next / repeat, volume; the idle card says Not playing or Spotify not installed with Open / Get Spotify. Mac strings, in every language already. - The Mac's BotEngine dance (setDancing, applyDance, happy eyes) in mochi/engine.ts, with the Mac's rules: the compact island whenever music plays, expanded on the Spotify card, the pill's mini Mochi, and Mochi on the desktop (the island sends him `dancing`). Music starting shows the hidden island without the peek sound. On Windows nothing reports music, so nothing changes there. Tests: MPRIS parsing from dbus variants and plain values, position extrapolation, track ids, covers' sources and types (Rust, plus an end-to-end test against a fake Spotify on a private bus, run with `dbus-run-session -- cargo test -p coucou --lib spotify -- --ignored`); the dance math, the dance rules, the catalog's Linux-only filtering, report handling and the views (Node). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJm3SRL9k3gBPaVtfPhwcT
Cargo.toml: one dbus line for both portal.rs (#367) and linux_focus.rs, x11rb kept. README: the Linux remark on Open terminal and #367's shortcuts line. The portal test from #367 still expected "Send Mochi to the desktop" to be unported; #370 ported it, so it is handed over like the others now, and "Attach the front window" is the one left unported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJm3SRL9k3gBPaVtfPhwcT
# Conflicts: # windows/README.md # windows/src-tauri/Cargo.toml # windows/src-tauri/src/lib.rs
# Conflicts: # windows/src-tauri/Cargo.toml
shawn-t516
pushed a commit
to shawn-t516/coucou
that referenced
this pull request
Oct 9, 2026
The macOS runner failed this test on main and on Louis-CFM#372 while nothing it covers had changed. The two cases that raced a short timer against a short sleep now check long after the old timer and long before the new one, and every wait gets the 5 s default instead of 1 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJm3SRL9k3gBPaVtfPhwcT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Mac's Spotify pill (#246) comes to Linux, through MPRIS, and Mochi now dances to the music on Windows/Linux, by the Mac's rules. Windows has no music source yet, so nothing changes there (same code path, nothing ever reports a track).
Rust:
src-tauri/src/spotify.rs(Linux only)org.mpris.MediaPlayer2.spotifyon the session bus, usingdbus0.9.12. It is already inCargo.lockthrough keyring, so nothing new is downloaded. The only lock change is the new dependency line. Linux: global shortcuts on Wayland through the XDG GlobalShortcuts portal #367 addsdbusthe same way.PropertiesChanged,SeekedandNameOwnerChanged(Spotify starting or quitting). Undeclaring the pill sends that thread a ping so it leaves the bus at once. Theblocking_pop_messagetimeout is one hour; it only bounds a lost wake-up.mpris:length,mpris:artUrl),PlaybackStatus,Position,Shuffle,LoopStatusandVolume. Titles and artists are cut like the Mac'sshortTitle/shortArtist. Track ids becomespotify:track:…, so ads are recognised.spotify_refresh). The page runs it on frompositionAt, asposition(at:)does on the Mac.spotify_control: playPause / next / previous / seek / shuffle / repeat / volume. Seek usesSetPosition, orSeekon builds whose trackid isn't an object path. Shuffle, repeat and volume show at once, since Spotify doesn't always signal them.spotify_open: Raise if it can. Otherwise it startsspotifyfrom PATH, else the Flatpakcom.spotify.Client, else the Snap. With none of them it opens https://www.spotify.com/download/linux/.spotify_installed: for Settings.img-src 'self' data: blob:), so Rust fetches the cover with reqwest and sends a data URL in aspotify-artworkevent. Rules:i.scdn.co,*.scdn.co,*.spotifycdn.comor a localfile://path are read.open.spotify.com/image/…is rewritten to the CDN.Page
integration_spotify("Spotify",#1DB954, service, "Integration"), connectnone. A newsupport: "linux"sits next to"windows". It is declared in Settings → Integrations, which now lists only the pills this OS offers. When there is no Spotify to launch, the row says "Not installed", as on the Mac.views/spotify.ts) are ported fromSpotifyPill,MusicControlButton,SpotifyCardViewandNowPlaying*, with the Mac's sizes and colours:strings.json("Not playing", "Open Spotify", "Get Spotify", "{0} — open Spotify", "Volume {0}%", "Shuffle on"…).extra.jsonneeded nothing new.syncTaskNamedoes. When music starts, the hidden island opens to compact without the peek sound (musicReveal).Dance
BotEngine.setDancing/applyDanceare ported exactly intomochi/engine.ts: 112 BPM sway, hop, tilt and squash around the bottom of the body, a 0.3 s fade in and 0.5 s fade out, and happy eyes in idle or finished.dancingto the snapshot it sends him.README
The Linux section now covers the Spotify pill. The "doesn't dance" line now says he dances to Spotify on Linux.
Tests
cargo test -p coucou --libgives 213 passed and 1 ignored (11 new). They cover:dbusvariants and from plain values, and the metadata order.dbus-run-session -- cargo test -p coucou --lib spotify -- --ignoredpasses. It runs the real listener against a fake Spotify on a private session bus:PropertiesChangedthat comes back and the position read again;NameOwnerChanged);npm testgives 345 passed (21 new). They cover:npx tsc --noEmitis clean, andcargo build -p coucouhas no warnings.Not covered
🤖 Generated with Claude Code
https://claude.ai/code/session_01LJm3SRL9k3gBPaVtfPhwcT
Generated by Claude Code