{ 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; }; }; }