nixos/shared/modules/services/theme_switcher/default.nix
2026-01-23 13:44:07 -07:00

349 lines
13 KiB
Nix

{ config, lib, pkgs, ... }:
# Theme Switcher Module - Architecture Overview
#
# This module integrates wpaperd for wallpaper management with pywal for dynamic theming.
# wpaperd handles wallpaper rotation and transitions, while Python scripts handle palette
# generation and intelligent semantic color mapping.
#
# Python Scripts:
# - theme_switcher.py: Time-based light/dark theme switching
# - wallpaper_rotation.py: Continuous wallpaper rotation with theme regeneration
# - apply_themes.py: Applies GTK/Kvantum/Helix themes after pywal runs
# - color_mapper.py: Intelligent semantic color mapping (reds for errors, high contrast for comments)
#
# Two modes of operation:
#
# 1. Light/Dark Theme Switching (theme-switcher service, triggered by timer at configured times):
# - Updates wpaperd config to point to Light/ or Dark/ directory
# - Restarts wpaperd (which picks a random wallpaper with transition)
# - Generates pywal palette from the selected wallpaper
# - Runs semantic color mapper to assign colors based on hue analysis
# - Applies GTK/Kvantum/Helix themes with intelligent color assignments
#
# 2. Continuous Wallpaper Rotation (wallpaper-rotation service, triggered by timer every N minutes):
# - Pauses wpaperd's auto-rotation
# - Calls wpaperctl next-wallpaper (wpaperd handles the smooth transition)
# - Gets current wallpaper from wpaperd
# - Regenerates pywal palette from new wallpaper
# - Runs semantic color mapper for intelligent color assignment
# - Applies GTK/Kvantum/Helix themes
# - Resumes wpaperd's auto-rotation
#
# This approach leverages wpaperd's built-in features (transitions, random selection, timing)
# while keeping palette generation and theme application in sync with wallpaper changes.
# The semantic color mapper ensures consistent meaning (e.g., reds for errors) regardless of wallpaper.
let
cfg = config.services.themeSwitcher;
# Get home directory for the user
userConfig = config.home-manager.users.${cfg.user};
homeDir = userConfig.home.homeDirectory or "/home/${cfg.user}";
# Create a custom pywal16 with haishoku backend available
pywal16WithBackends = pkgs.pywal16.overridePythonAttrs (old: {
propagatedBuildInputs = (old.propagatedBuildInputs or []) ++ [
pkgs.python313Packages.haishoku
];
});
# Python scripts for theme management
themeSwitcherScript = pkgs.writeScript "theme-switcher" ''
#!${pkgs.python3}/bin/python3
import subprocess
import sys
# Execute the theme switcher Python script
result = subprocess.run([
"${./theme_switcher.py}",
"--wallpaper-path", "${cfg.wallpaperPath}",
"--light-time", "${cfg.lightTime}",
"--dark-time", "${cfg.darkTime}",
"--backend", "${cfg.backend}",
"--rotation-interval", "${cfg.rotation.interval}",
"--wpaperd-mode", "${cfg.wpaperd.mode}",
"--sorting", "${cfg.rotation.sorting}",
"--apply-themes-script", "${applyThemesScript}"
])
sys.exit(result.returncode)
'';
# Python script to apply GTK/Kvantum/Helix themes
applyThemesScript = ./apply_themes.py;
# Python script for wallpaper rotation
wallpaperRotationScript = pkgs.writeScript "wallpaper-rotation" ''
#!${pkgs.python3}/bin/python3
import subprocess
import sys
# Execute the wallpaper rotation Python script
result = subprocess.run([
"${./wallpaper_rotation.py}",
"--light-time", "${cfg.lightTime}",
"--dark-time", "${cfg.darkTime}",
"--backend", "${cfg.backend}",
"--apply-themes-script", "${applyThemesScript}"
])
sys.exit(result.returncode)
'';
in
{
options.services.themeSwitcher = {
enable = lib.mkEnableOption "dynamic theme switching based on time and wallpapers with integrated wallpaper management";
user = lib.mkOption {
type = lib.types.str;
description = "Username for theme switching";
};
wallpaperPath = lib.mkOption {
type = lib.types.str;
default = "${homeDir}/nixos/shared/modules/services/wallpapers";
defaultText = lib.literalExpression ''"''${config.home-manager.users.''${cfg.user}.home.homeDirectory}/nixos/shared/modules/services/wallpapers"'';
description = "Path to wallpaper directories (Light/ and Dark/ subdirectories expected)";
};
backend = lib.mkOption {
type = lib.types.enum [ "haishoku" "modern_colorthief" "fast_colorthief" "colorthief" "colorz" "wal" ];
default = "haishoku";
description = "Pywal color extraction backend";
};
lightTime = lib.mkOption {
type = lib.types.str;
default = "09:00:00";
description = "Time to switch to light theme (HH:MM:SS)";
};
darkTime = lib.mkOption {
type = lib.types.str;
default = "16:30:00";
description = "Time to switch to dark theme (HH:MM:SS)";
};
enableThemeSwitch = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable automatic time-based theme switching via systemd timer";
};
paletteGeneration = lib.mkOption {
type = lib.types.enum [ "light-dark" "continuous" ];
default = "continuous";
description = ''
When to regenerate color palettes from wallpapers:
- light-dark: Only regenerate at light/dark switch times (6 AM/6 PM)
- continuous: Regenerate palette every time wallpaper rotates
'';
};
# Wallpaper rotation options
rotation = {
interval = lib.mkOption {
type = lib.types.str;
default = "5m";
example = "30s";
description = "How often to rotate wallpapers (e.g., 30s, 5m, 1h)";
};
sorting = lib.mkOption {
type = lib.types.enum [ "random" ];
default = "random";
description = "Wallpaper selection method (currently only random is supported)";
};
};
# Wpaperd integration options
wpaperd = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable wpaperd wallpaper daemon integration";
};
mode = lib.mkOption {
type = lib.types.enum [ "center" "fit" "fit-border-color" "stretch" "tile" ];
default = "center";
description = "How to display wallpapers when size differs from display resolution";
};
transition = {
effect = lib.mkOption {
type = lib.types.str;
default = "fade";
example = "simple";
description = "Transition effect name (fade, simple, etc.)";
};
duration = lib.mkOption {
type = lib.types.int;
default = 300;
example = 2000;
description = "Transition duration in milliseconds";
};
};
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${cfg.user} = {
home.packages = with pkgs; [
pywal16WithBackends # pywal16 with haishoku backend included
imagemagick # Useful for image manipulation
wpaperd # Wallpaper daemon and CLI tool
];
# Install manual theme switching script to user's local bin
home.file.".local/bin/switch-theme.sh" = {
source = pkgs.writeShellScript "switch-theme.sh" ''
# Manual theme switching script
# Usage: switch-theme.sh [light|dark]
if [ $# -ne 1 ]; then
echo "Usage: $0 [light|dark]"
exit 1
fi
MODE="$1"
if [ "$MODE" != "light" ] && [ "$MODE" != "dark" ]; then
echo "Error: Mode must be 'light' or 'dark'"
exit 1
fi
# Trigger the theme switcher service which will handle everything
${pkgs.systemd}/bin/systemctl --user start theme-switcher.service
'';
executable = true;
};
# Pywal custom templates for GTK, Kvantum, and Helix
xdg.configFile."wal/templates/gtk-3.0.css".source = ./templates/gtk-3.0.css;
xdg.configFile."wal/templates/gtk-4.0.css".source = ./templates/gtk-4.0.css;
xdg.configFile."wal/templates/PywalTheme.kvconfig".source = ./templates/PywalTheme.kvconfig;
xdg.configFile."wal/templates/helix_minimal.toml".source = ./templates/helix_minimal.toml;
xdg.configFile."wal/templates/helix_semantic.toml".source = ./templates/helix_semantic.toml;
# Start wpaperd daemon if enabled
# Note: We don't use services.wpaperd from home-manager because we need to
# dynamically update the config file, which would be read-only if managed by home-manager
systemd.user.services.wpaperd = lib.mkIf cfg.wpaperd.enable {
Unit = {
Description = "Wallpaper daemon for Wayland";
After = [ "graphical-session.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
Type = "simple";
# Create initial config before starting if it doesn't exist
ExecStartPre = pkgs.writeShellScript "wpaperd-init.sh" ''
mkdir -p ~/.config/wpaperd
if [ ! -f ~/.config/wpaperd/config.toml ] || [ -L ~/.config/wpaperd/config.toml ]; then
# Remove old symlink if exists and create initial config
rm -f ~/.config/wpaperd/config.toml
# Determine initial directory based on current time
hour=$(${pkgs.coreutils}/bin/date +%H)
light_hour=$(echo "${cfg.lightTime}" | ${pkgs.coreutils}/bin/cut -d: -f1)
dark_hour=$(echo "${cfg.darkTime}" | ${pkgs.coreutils}/bin/cut -d: -f1)
if [ "$hour" -ge "$light_hour" ] && [ "$hour" -lt "$dark_hour" ]; then
WALLPAPER_DIR="${cfg.wallpaperPath}/Light"
else
WALLPAPER_DIR="${cfg.wallpaperPath}/Dark"
fi
cat > ~/.config/wpaperd/config.toml << EOF
[default]
path = "$WALLPAPER_DIR"
duration = "${cfg.rotation.interval}"
mode = "${cfg.wpaperd.mode}"
sorting = "${cfg.rotation.sorting}"
EOF
fi
'';
ExecStart = "${pkgs.wpaperd}/bin/wpaperd";
Restart = "on-failure";
};
Install.WantedBy = [ "graphical-session.target" ];
};
# Systemd timer and service for theme switching at light/dark times
systemd.user.timers.theme-switcher = lib.mkIf cfg.enableThemeSwitch {
Unit.Description = "Dynamic Theme Switcher Timer";
Timer = {
OnCalendar = [ "*-*-* ${cfg.lightTime}" "*-*-* ${cfg.darkTime}" ];
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
systemd.user.services.theme-switcher = lib.mkIf cfg.enableThemeSwitch {
Unit.Description = "Dynamic Theme Switcher Service";
Service = {
Type = "oneshot";
ExecStart = "${themeSwitcherScript}";
# Use home-manager profile to get pywal with all backends
Environment = [
"PATH=${config.home-manager.users.${cfg.user}.home.profileDirectory}/bin:${lib.makeBinPath (with pkgs; [
python3
systemd
coreutils
findutils
glib
libsForQt5.qtstyleplugin-kvantum
bash
wpaperd
procps # for pgrep
])}"
];
};
};
# Wallpaper rotation timer and service (for continuous palette generation)
systemd.user.timers.wallpaper-rotation = lib.mkIf (cfg.paletteGeneration == "continuous") {
Unit.Description = "Wallpaper Rotation Timer (with Theme Regeneration)";
Timer = {
OnBootSec = "1min"; # Start 1 minute after boot
OnUnitActiveSec = cfg.rotation.interval;
Persistent = false;
};
Install.WantedBy = [ "timers.target" ];
};
systemd.user.services.wallpaper-rotation = lib.mkIf (cfg.paletteGeneration == "continuous") {
Unit.Description = "Wallpaper Rotation Service (with Theme Regeneration)";
Service = {
Type = "oneshot";
ExecStart = "${wallpaperRotationScript}";
# Use home-manager profile to get pywal with all backends
Environment = [
"PATH=${config.home-manager.users.${cfg.user}.home.profileDirectory}/bin:${lib.makeBinPath (with pkgs; [
python3
systemd
coreutils
findutils
glib
libsForQt5.qtstyleplugin-kvantum
bash
wpaperd
procps # for pgrep
])}"
];
};
};
# Keep Qt/GTK enabled but allow pywal to override themes
qt = {
enable = true;
platformTheme.name = "kvantum";
style.name = "kvantum";
};
gtk.enable = true;
};
};
}