quickshell: open wallpaper dropdown on hover-switch like other menus

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-19 17:50:59 +01:00
parent c87b40d528
commit 9447413fa1
6 changed files with 35 additions and 224 deletions

View file

@ -51,188 +51,6 @@ in
};
};
# spotify-player — native librespot TUI replacing the Electron Spotify app.
# stylix's spotify-player target writes the colour theme.
# MPRIS (quickshell's media card), notifications and streaming are all on by
# default on Linux, so only the off-by-default visualiser needs setting.
programs.spotify-player = lib.mkIf isDesktop {
enable = true;
# 26.05 ships 0.23.0, whose Web API token is cached with refresh_token =
# null: once it expires (~1h) nothing can renew it and the library just
# spins until the app is restarted. 0.24.1 treats a non-renewable cached
# token as unusable and re-runs the PKCE flow instead. Drop this override
# once the pinned channel reaches 0.24.1.
package = pkgs.spotify-player.overrideAttrs (old: rec {
version = "0.24.1";
src = pkgs.fetchFromGitHub {
owner = "aome510";
repo = "spotify-player";
tag = "v${version}";
hash = "sha256-+GADmRl4XMwV8TfYZjEeyKDDfda3bDPzeerhYryX6vA=";
};
cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
inherit src;
hash = "sha256-CSZ5sZ+d7Jhi43ipaWXKupYPFgWCbCx4RMTQN8emu9o=";
};
# Give initialize_playback's retry loop a much longer window, so we don't
# have to pick our own device from the popup on every launch. Upstream
# retries the transfer 5 times, once a second; our librespot device isn't
# registered with the Spotify backend until ~20s after startup, so all
# five attempts 404 and it gives up:
# Trying to connect to device (id=…)
# Connection failed (device_id=…): http error: status code 404 Not Found
# The loop breaks on the first success, so a bigger bound costs nothing.
# The CLI is no help here: `connect --name` resolves names against the Web
# API device list, which doesn't contain the integrated device at all.
#
# Widen it by slowing the delay rather than by adding rounds: each round
# costs up to three Web API calls, and we now share the bundled client ID
# with every other spotify-player/ncspot user, so 60 one-second rounds was
# ~180 requests per launch into a shared rate-limit bucket (429s). 12
# rounds five seconds apart covers the same 60s for a fifth of the calls.
# Both loops in this file are `for _ in 0..5` / `from_secs(1)`; the leading
# indentation is what pins each replacement to the intended one.
#
# The third replacement hits the other loop, in retry_playback_state: it
# re-reads the playback state five times after *every* playback-changing
# action, so one flick of the volume wheel costs a write plus five reads
# per notch. The state is updated optimistically client-side anyway, so
# two confirmations are plenty.
postPatch = (old.postPatch or "") + ''
substituteInPlace spotify_player/src/client/mod.rs \
--replace-fail " for _ in 0..5 {" \
" for _ in 0..12 {" \
--replace-fail " let delay = std::time::Duration::from_secs(1);" \
" let delay = std::time::Duration::from_secs(5);" \
--replace-fail " for _ in 0..5 {" \
" for _ in 0..2 {"
'';
});
settings = {
enable_audio_visualization = true;
# No client_id: use the bundled default (ncspot's), as upstream
# recommends. It predates Spotify's 2024-11-27 Web API change and is in
# extended quota mode, so it still gets the editorial/algorithmic
# playlists — Discover Weekly, Made-For-You, browse categories. Any app
# registered today is stuck in restricted quota and Spotify returns those
# as null, which is what left Made-For-You empty while we ran our own.
#
# We did briefly run our own app (private rate-limit bucket) because the
# bundled one's bucket is shared with every other spotify-player/ncspot
# user and we hit 429s. But that burst came from scroll-to-set-volume and
# the post-action confirmation reads, both cut below — so the private
# bucket is no longer worth losing half the library over.
#
# Changing client ID invalidates the cached Web API token: run
# `spotify_player authenticate` once after deploying this.
#
# Scroll-to-set-volume is a rate-limit trap: playback runs through a local
# librespot softmixer, but the mixer is only set once at session start, so
# every later change is a Web API PUT /me/player/volume round-trip (plus
# confirmation reads) to adjust a mixer inside our own process. One flick
# of the wheel is a dozen requests and a 429. Use PipeWire for volume
# instead — same sink, instant, free.
enable_mouse_scroll_volume = false;
# Streaming stays on in the TUI (the default). A `spotify_player --daemon`
# holding the device, with enable_streaming = "DaemonOnly" here, does move
# the ~20s device registration to login — but the visualiser above is fed
# only by the in-process librespot player (state/mod.rs vis_bands, written
# solely from streaming.rs), so a TUI that isn't the player can never draw
# it. Visualiser and daemon are mutually exclusive; the visualiser wins.
};
# Rectangle gauges only ever use `playback_progress_bar`: ratatui paints the
# filled part in its fg and the rest in its bg, and `_unfilled` is ignored
# (it is Line-only). The upstream default is fg Green / bg BrightBlack, and
# stylix collapses the whole 16-colour palette onto three colours — Green
# #41afb2 and BrightBlack #3fa8ab are the same teal by eye, so the bar
# rendered as one solid block with no visible progress. Repainting it
# foreground-on-background is the only pair stylix guarantees is distinct.
# mkForce because stylix owns this list and find_theme takes the *first*
# match by name, so an appended second "stylix" would be ignored; the
# palette below reproduces stylix's own mapping so nothing else changes.
themes = lib.mkForce [
{
name = "stylix";
palette = with config.lib.stylix.colors.withHashtag; {
background = base00;
foreground = base05;
black = base00;
red = base08;
green = base0B;
yellow = base0A;
blue = base0D;
magenta = base0E;
cyan = base0C;
white = base05;
bright_black = base03;
bright_red = base08;
bright_green = base0B;
bright_yellow = base0A;
bright_blue = base0D;
bright_magenta = base0E;
bright_cyan = base0C;
bright_white = base07;
};
component_style = {
playback_progress_bar = {
fg = "BrightWhite";
bg = "Black";
};
playback_progress_bar_unfilled.bg = "Black";
};
}
];
# The home-manager module only writes keymap.toml when `keymaps` is
# non-empty, so `actions` alone would silently emit nothing. Restating a
# default binding is the cheapest way to make the file exist.
keymaps = [
{
command = "ShowActionsOnCurrentTrack";
key_sequence = "a";
}
];
# Actions default to the *selected* item, which is useless when listening
# along to someone else's playback — the track was never selected, so
# liking it means finding it by hand. target = "PlayingTrack" acts on
# whatever is actually playing.
actions = [
{
action = "ToggleLiked";
key_sequence = "L";
target = "PlayingTrack";
}
{
action = "GoToArtist";
key_sequence = "g P";
target = "PlayingTrack";
}
{
action = "GoToAlbum";
key_sequence = "g B";
target = "PlayingTrack";
}
];
};
# Upstream ships no .desktop file (TUI only), so the launcher can't find it.
# Hyprland parks the instance started at login on the special:music workspace
# (see settings/hyprland.nix), so this toggles that window rather than opening
# a second terminal — a fresh process would mean another ~20s wait for its
# Spotify Connect device. Relaunch first if the window is gone, otherwise the
# toggle just reveals an empty workspace.
xdg.desktopEntries.spotify-player = lib.mkIf isDesktop {
name = "Spotify Player";
genericName = "Music Player";
exec = "${pkgs.writeShellScript "spotify-player-toggle" ''
hyprctl clients | grep -q "title: spotify-player" \
|| ghostty --title=spotify-player -e spotify_player
hyprctl dispatch togglespecialworkspace music
''}";
icon = "spotify";
terminal = false;
categories = [ "Audio" "Music" "Player" "AudioVideo" ];
};
# Warm Ghostty instance — new windows attach via gtk-single-instance.
systemd.user.services.ghostty-daemon = lib.mkIf isDesktop {
Unit = {