349 lines
13 KiB
Nix
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;
|
|
};
|
|
};
|
|
}
|