267 lines
11 KiB
Nix
267 lines
11 KiB
Nix
# home-manager/fred.nix
|
|
{ config, pkgs, lib, inputs, osConfig, ... }:
|
|
let
|
|
isDesktop = lib.elem osConfig.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ];
|
|
in
|
|
{
|
|
home.stateVersion = "25.11";
|
|
|
|
home.packages = with pkgs; [
|
|
#
|
|
];
|
|
|
|
home.file.".config/nixpkgs/config.nix".text = ''
|
|
{ allowUnfree = true; }
|
|
'';
|
|
|
|
# Wallpaper source — consumed by stylix's image option (set in
|
|
# settings/stylix.nix) and by quickshell's wallpaper layer.
|
|
home.file.".local/share/backgrounds/wallpaper.png".source =
|
|
"${inputs.self}/walls/wallpaper.png";
|
|
|
|
# btop — stylix's btop target writes the colour theme; we keep our
|
|
# non-colour preferences here.
|
|
programs.btop = {
|
|
enable = true;
|
|
settings = {
|
|
theme_background = false;
|
|
vim_keys = false;
|
|
proc_filter_kernel = true;
|
|
};
|
|
};
|
|
|
|
# Ghostty — stylix's ghostty target writes the colour theme.
|
|
programs.ghostty = lib.mkIf isDesktop {
|
|
enable = true;
|
|
settings = {
|
|
font-family = "CaskaydiaCove NF";
|
|
font-size = 11;
|
|
font-thicken = true;
|
|
window-padding-x = 10;
|
|
window-padding-y = 10;
|
|
window-padding-balance = true;
|
|
background-opacity = 0.98;
|
|
confirm-close-surface = false;
|
|
gtk-titlebar = false;
|
|
gtk-single-instance = true;
|
|
# keeps the warm instance alive with zero windows
|
|
quit-after-last-window-closed = false;
|
|
cursor-style = "bar";
|
|
cursor-style-blink = true;
|
|
};
|
|
};
|
|
|
|
# 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;
|
|
# The librespot device is only usable once Spotify's backend has accepted
|
|
# it, which takes ~20s from process start — that is the wait before you can
|
|
# play anything, and it is server-side, so there is nothing local to speed
|
|
# up. Instead, pay it once at login: the daemon below holds the device for
|
|
# the whole session, and DaemonOnly keeps the TUI from registering a second
|
|
# one, so opening the TUI is instant and plays straight onto the daemon's
|
|
# device.
|
|
enable_streaming = "DaemonOnly";
|
|
};
|
|
# 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";
|
|
}
|
|
];
|
|
};
|
|
|
|
# Holds the librespot Spotify Connect device for the whole session, so the
|
|
# ~20s the Spotify backend takes to accept a new device is paid once at login
|
|
# instead of on every launch. Also means the MPRIS card in quickshell keeps
|
|
# working with no terminal open. Type=forking because the daemonize crate
|
|
# double-forks and the parent exits; it writes no pidfile, so systemd guesses.
|
|
systemd.user.services.spotify-player-daemon = lib.mkIf isDesktop {
|
|
Unit = {
|
|
Description = "spotify-player daemon (holds the Spotify Connect device)";
|
|
After = [ "graphical-session.target" "pipewire.service" ];
|
|
PartOf = [ "graphical-session.target" ];
|
|
};
|
|
Service = {
|
|
Type = "forking";
|
|
ExecStart = "${config.programs.spotify-player.package}/bin/spotify_player --daemon";
|
|
Restart = "on-failure";
|
|
RestartSec = 10;
|
|
};
|
|
Install.WantedBy = [ "graphical-session.target" ];
|
|
};
|
|
|
|
# Upstream ships no .desktop file (TUI only), so the launcher can't find it.
|
|
# enable_media_control=false: media-control is not gated on `is_daemon`, so
|
|
# without this both the daemon and the TUI export org.mpris.MediaPlayer2 and
|
|
# quickshell would show two cards for one player. The daemon owns MPRIS.
|
|
xdg.desktopEntries.spotify-player = lib.mkIf isDesktop {
|
|
name = "Spotify Player";
|
|
genericName = "Music Player";
|
|
exec = "ghostty --title=spotify-player -e spotify_player -o enable_media_control=false";
|
|
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 = {
|
|
Description = "Ghostty warm instance (no window)";
|
|
After = [ "graphical-session.target" ];
|
|
PartOf = [ "graphical-session.target" ];
|
|
};
|
|
Service = {
|
|
Type = "simple";
|
|
ExecStart = "${pkgs.ghostty}/bin/ghostty --initial-window=false";
|
|
Restart = "always";
|
|
};
|
|
Install.WantedBy = [ "graphical-session.target" ];
|
|
};
|
|
|
|
}
|