add kiki plymouth boot, remove old wallpaper switcher stuff, add background images

This commit is contained in:
Nathan Anderson 2026-02-10 21:22:34 -07:00
parent a0ff06bf8c
commit d4e3f950d7
60 changed files with 76 additions and 4473 deletions

View File

@ -1,4 +1,7 @@
# dotfiles/
.git/
flake.lock
frame12/framework-plymouth-theme
frame12/framework-plymouth-theme/framework/throbber-*
*.png
*.jpeg

View File

@ -3,6 +3,7 @@
//
// gui startup
spawn-at-startup "swaybg" "-i" "/home/nate/nixos/frame12/wallpaper.png" "-m" "fill"
spawn-at-startup "waybar"
spawn-at-startup "keepassxc"
spawn-at-startup "flatpak" "run" "org.signal.Signal"

View File

@ -65,6 +65,19 @@ in
nixpkgs.config.allowUnfree = true;
boot = {
plymouth = {
enable = true;
theme = "kiki";
themePackages = [
(pkgs.runCommand "plymouth-kiki-theme" { } ''
mkdir -p $out/share/plymouth/themes/kiki
cp -r ${./kiki-plymouth-theme/kiki}/* $out/share/plymouth/themes/kiki/
substituteInPlace $out/share/plymouth/themes/kiki/kiki.plymouth \
--replace-fail "@IMAGEDIR@" "$out/share/plymouth/themes/kiki"
'')
];
};
# Enable "Silent Boot"
consoleLogLevel = 0;
initrd.verbose = false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,54 @@
[Plymouth Theme]
Name=Kiki
Description=Theme with animated Kiki from Kiki's Delivery Service lying in grass.
ModuleName=two-step
[two-step]
Font=Cantarell 12
TitleFont=Cantarell Light 30
ImageDir=@IMAGEDIR@
DialogHorizontalAlignment=.5
DialogVerticalAlignment=.382
TitleHorizontalAlignment=.5
TitleVerticalAlignment=.382
HorizontalAlignment=.5
VerticalAlignment=.5
WatermarkHorizontalAlignment=.5
WatermarkVerticalAlignment=.96
Transition=none
TransitionDuration=0.0
BackgroundStartColor=0x498172
BackgroundEndColor=0x759B65
ProgressBarBackgroundColor=0x3a6b5e
ProgressBarForegroundColor=0xffffff
MessageBelowAnimation=true
[boot-up]
UseEndAnimation=false
[shutdown]
UseEndAnimation=false
[reboot]
UseEndAnimation=false
[updates]
SuppressMessages=true
ProgressBarShowPercentComplete=true
UseProgressBar=true
Title=Installing Updates...
SubTitle=Do not turn off your computer
[system-upgrade]
SuppressMessages=true
ProgressBarShowPercentComplete=true
UseProgressBar=true
Title=Upgrading System...
SubTitle=Do not turn off your computer
[firmware-upgrade]
SuppressMessages=true
ProgressBarShowPercentComplete=true
UseProgressBar=true
Title=Upgrading Firmware...
SubTitle=Do not turn off your computer

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -3,6 +3,7 @@
//
// gui startup
spawn-at-startup "swaybg" "-i" "/home/jaci/nixos/jaci/kiki_background.jpg" "-m" "fill"
spawn-at-startup "waybar"
spawn-at-startup "keepassxc"
// shell startup

View File

@ -24,7 +24,7 @@
# Stylix theming - auto-generate color scheme from wallpaper
stylix = {
enable = true;
image = ../shared/modules/services/wallpapers/Dark/IU-Dark.jpg;
image = ./wallpaper.png;
polarity = "dark";
# System-wide cursor

View File

@ -3,6 +3,7 @@
//
// gui startup
spawn-at-startup "swaybg" "-i" "/home/nate/nixos/nate-work/wallpaper.png" "-m" "fill"
spawn-at-startup "waybar"
spawn-at-startup "keepassxc"
spawn-at-startup "flatpak" "run" "org.signal.Signal"

BIN
nate-work/wallpaper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@ -23,7 +23,7 @@
# Stylix theming - auto-generate color scheme from wallpaper
stylix = {
enable = true;
image = ../shared/modules/services/wallpapers/Dark/FSO-Dark.jpg;
image = ./wallpaper.png;
polarity = "dark";
# System-wide cursor

BIN
nate/wallpaper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,74 +0,0 @@
# Example configuration for enabling the theme switcher module
# Add this to your host's desktop-configuration.nix or similar file
{
# Import the module (if not already in your imports list)
imports = [
../../shared/modules/services/theme_switcher
];
# Enable and configure the theme switcher
services.themeSwitcher = {
enable = true;
user = "nate"; # Change to your username
# Theme generation settings
backend = "haishoku"; # Recommended color extraction backend
lightTime = "06:00:00"; # Switch to light theme at 6 AM
darkTime = "18:00:00"; # Switch to dark theme at 6 PM
enableAutoSwitch = true; # Enable automatic time-based switching
# Palette generation strategy
paletteGeneration = "continuous"; # Options: continuous, light-dark
# Wallpaper rotation settings
rotation = {
interval = "5m"; # Rotate wallpaper every 5 minutes
sorting = "random"; # Currently only random is supported
};
# Wpaperd wallpaper daemon settings
wpaperd = {
enable = true;
mode = "center"; # Options: center, fit, fit-border-color, stretch, tile
transition = {
effect = "fade"; # Transition effect (fade, simple, etc.)
duration = 300; # Transition duration in milliseconds
};
};
};
# IMPORTANT: Disable the old wallpaper-rotator module if you were using it
# services.wallpaperRotator.enable = false; # Comment out or remove
# Note: You may want to comment out or remove static theme configurations
# when enabling the dynamic theme switcher. For example:
# BEFORE (static theme) - COMMENT THESE OUT:
# home-manager.users.nate = {
# gtk.theme = {
# name = "catppuccin-macchiato-lavender-compact+rimless";
# package = pkgs.catppuccin-gtk.override { ... };
# };
#
# xdg.configFile."Kvantum/kvantum.kvconfig".source = ...;
# };
# AFTER (dynamic theme):
# The theme switcher module handles GTK/Qt theming automatically
# You can still keep icon and cursor themes:
# home-manager.users.nate = {
# gtk.iconTheme = {
# name = "Papirus-Dark";
# package = pkgs.catppuccin-papirus-folders;
# };
# gtk.cursorTheme = {
# name = "Bibata-Modern-Classic";
# package = pkgs.bibata-cursors;
# };
# };
}
# For manual testing before enabling auto-switch:
# services.themeSwitcher.enableAutoSwitch = false;
# Then use: ~/.local/bin/apply-theme.sh --light (or --dark)

View File

@ -1,399 +0,0 @@
# NixOS Dynamic Theme Switcher - Implementation Instructions
## Objective
Build a time-based dynamic theme switching system on NixOS that:
1. Extracts color palettes from wallpapers using pywal16 with haishoku backend
2. Generates and applies themes for GTK, Qt/Kvantum, Ghostty, and other applications
3. Switches between light/dark themes based on time of day
4. Integrates with existing wpaperd wallpaper rotation service
## Current System State
### Existing Theming Configuration
**GTK Configuration:**
- Current theme: `catppuccin-macchiato-lavender-compact+rimless`
- Settings location: `~/.config/gtk-3.0/settings.ini`, `~/.config/gtk-4.0/settings.ini`
- Icon theme: `Papirus-Dark`
- Cursor: `Bibata-Modern-Classic`
- Files are symlinked from Nix store via home-manager
**Qt/Kvantum Configuration:**
- Current theme: `catppuccin-macchiato-lavender`
- Location: `~/.config/Kvantum/kvantum.kvconfig`
- Style: kvantum (both Qt5 and Qt6)
- Environment variable: `QT_STYLE_OVERRIDE=kvantum`
**Wallpaper Service:**
- Using wpaperd with home-manager integration
- Wallpapers in: `/home/nate/nixos/shared/modules/services/wallpapers/`
- Organized into `Light/` and `Dark/` subdirectories
- Service configured via `services.wallpaperRotator` module
**Actions Required:**
1. Disable/modify static GTK theme configuration in home.nix
2. Disable/modify static Kvantum theme configuration
3. Integrate with wpaperd to trigger theme changes on wallpaper rotation
4. Remove hardcoded Catppuccin theme packages when pywal themes are working
## Implementation Components
### 3. Install Required Packages
Add to module configuration:
```nix
home.packages = with pkgs; [
pywal16 # Color scheme generator with 16-color support
python3Packages.haishoku # Recommended backend for pywal16
wpaperd # Already installed via wallpaper service
];
```
**Available pywal16 backends:** modern_colorthief, wal, fast_colorthief, haishoku, colorthief, colorz, okthief, schemer2
### 4. Color Extraction & Theme Generation
#### 4.1 Pywal16 Built-in Templates
**Pywal16 already includes templates for:**
- Ghostty: `ghostty.conf` template (tested and confirmed)
- CSS variables: `colors.css` template
- Waybar: `colors-waybar.css`
- Hyprland: `colors-hyprland.conf`
- Rofi: `colors-rofi-dark.rasi` and `colors-rofi-light.rasi`
- Sway: `colors-sway`
- Mako: `colors-mako`
- Foot, Kitty, Alacritty terminal configs
- Many others (47 templates total)
**Output location:** `~/.cache/wal/` after running `wal -i /path/to/wallpaper`
**Usage:**
```bash
wal -i /path/to/wallpaper --backend haishoku -n
# -n flag skips setting wallpaper (wpaperd handles that)
# --backend haishoku uses the haishoku color extraction
# -l flag for light themes
```
#### 4.2 GTK Theme Generation
**Goal:** Create custom GTK 3.0 and GTK 4.0 CSS themes from pywal colors
**Note:** Pywal16 includes `colors.css` template with CSS variables, but NOT full GTK themes.
**Required files:**
- `~/.config/gtk-3.0/gtk.css` - Custom CSS overlay
- `~/.config/gtk-4.0/gtk.css` - Custom CSS overlay
**Approach:** Create pywal template to generate GTK CSS using colors.json output
- Map pywal colors to GTK color definitions
- Use @define-color for GTK theme variables
- Apply to common widgets
#### 4.3 Qt/Kvantum Theme Generation
**Goal:** Generate Kvantum theme from pywal colors
**Kvantum structure (.kvconfig file):**
- `[General]` section with metadata
- `[GeneralColors]` section with color definitions (window.color, base.color, button.color, etc.)
- `[Hacks]` section with tweaks
- Per-widget sections (PanelButtonCommand, etc.)
**Location:** `~/.config/Kvantum/PywalTheme/PywalTheme.kvconfig`
**Required:** Create pywal template that maps colors.json to .kvconfig format
**Optional:** SVG file can be minimal or omitted for basic themes
**Application:** Use `kvantummanager --set PywalTheme` to activate
### 5. Theme Application Script
**Script: `apply-theme.sh`**
**Parameters:**
- `--light` or `--dark` (theme mode)
- Optional: `--wallpaper-path` (specific wallpaper, otherwise use wpaperd current)
**Script Logic:**
1. **Determine wallpaper source:**
- If `--wallpaper-path` provided, use it
- Otherwise, let wpaperd select based on mode (Light/ or Dark/ directory)
- Use `wpaperctl` to interact with wpaperd if needed
2. **Generate color scheme:**
```bash
if [ "$MODE" = "light" ]; then
wal -i "$WALLPAPER" --backend haishoku -l -n
else
wal -i "$WALLPAPER" --backend haishoku -n
fi
```
3. **Apply GTK theme:**
```bash
# pywal doesn't generate full GTK themes, use custom CSS
ln -sf ~/.cache/wal/gtk-3.0.css ~/.config/gtk-3.0/gtk.css
ln -sf ~/.cache/wal/gtk-4.0.css ~/.config/gtk-4.0/gtk.css
gsettings set org.gnome.desktop.interface color-scheme "prefer-$MODE"
```
4. **Apply Qt/Kvantum theme:**
```bash
kvantummanager --set PywalTheme
```
5. **Reload configurations:**
- Ghostty: Auto-reloads config on file change (no action needed)
- GTK apps: Automatically detect gsettings changes
- Qt apps: May need restart for Kvantum changes
- Hyprland/Sway: Reload via IPC if using pywal templates
6. **Optional: Update Flatpak:**
```bash
flatpak override --user --filesystem=~/.cache/wal:ro
```
### 6. Wallpaper Integration & Time-Based Switching
**Option A: Integrate with wpaperd events (Recommended)**
- Modify wallpaper-rotator module to support Light/Dark subdirectories
- Change wallpaper directory based on time of day
- Trigger theme generation on wallpaper change
**Option B: Independent systemd timer**
**Timer configuration:**
```nix
systemd.user.timers.theme-switcher = {
Unit.Description = "Dynamic Theme Switcher Timer";
Timer = {
OnCalendar = [ "*-*-* 06:00:00" "*-*-* 18:00:00" ]; # 6am light, 6pm dark
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
```
**Service configuration:**
```nix
systemd.user.services.theme-switcher = {
Unit.Description = "Dynamic Theme Switcher";
Service = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash ${./scripts/theme-switcher.sh}";
Environment = "PATH=${lib.makeBinPath [ pkgs.pywal16 pkgs.coreutils ]}";
};
};
```
**Script determines light/dark based on current hour:**
```bash
hour=$(date +%H)
if [ $hour -ge 6 ] && [ $hour -lt 18 ]; then
MODE="light"
WALLPAPER_DIR="Light"
else
MODE="dark"
WALLPAPER_DIR="Dark"
fi
```
### 7. NixOS Module Structure
**Module: `theme_switcher/default.nix`**
```nix
{ config, lib, pkgs, ... }:
let
cfg = config.services.themeSwitcher;
in
{
options.services.themeSwitcher = {
enable = lib.mkEnableOption "dynamic theme switching based on time and wallpapers";
user = lib.mkOption {
type = lib.types.str;
description = "Username for theme switching";
};
wallpaperPath = lib.mkOption {
type = lib.types.str;
default = "/home/${cfg.user}/nixos/shared/modules/services/wallpapers";
description = "Path to wallpaper directories (Light/ and Dark/)";
};
backend = lib.mkOption {
type = lib.types.enum [ "haishoku" "modern_colorthief" "fast_colorthief" ];
default = "haishoku";
description = "Pywal color extraction backend";
};
lightTime = lib.mkOption {
type = lib.types.str;
default = "06:00:00";
description = "Time to switch to light theme (HH:MM:SS)";
};
darkTime = lib.mkOption {
type = lib.types.str;
default = "18:00:00";
description = "Time to switch to dark theme (HH:MM:SS)";
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${cfg.user} = {
home.packages = with pkgs; [
pywal16
python3Packages.haishoku
];
# Install theme application scripts
home.file.".local/bin/apply-theme.sh" = {
source = ./scripts/apply-theme.sh;
executable = true;
};
# Pywal custom templates for GTK and Kvantum
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/kvantum.kvconfig".source = ./templates/kvantum.kvconfig;
# Systemd timer and service
systemd.user.timers.theme-switcher = { /* ... */ };
systemd.user.services.theme-switcher = { /* ... */ };
# Keep Qt/GTK enabled but don't set static themes
qt = {
enable = true;
platformTheme.name = "kvantum";
style.name = "kvantum";
};
gtk.enable = true;
};
};
}
```
**Template files needed:**
- `templates/gtk-3.0.css` - GTK3 theme using pywal variables
- `templates/gtk-4.0.css` - GTK4 theme using pywal variables
- `templates/kvantum.kvconfig` - Kvantum config using pywal colors
## Testing & Validation
### 8. Test Plan
1. **Test pywal color extraction:**
```bash
# Test with light wallpaper
wal -i ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg --backend haishoku -l -n
cat ~/.cache/wal/colors.json # Verify colors extracted
# Test with dark wallpaper
wal -i ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg --backend haishoku -n
cat ~/.cache/wal/colors.json # Verify colors extracted
```
2. **Verify pywal template output:**
```bash
ls -la ~/.cache/wal/
# Should see: colors.json, ghostty.conf, colors.css, and custom templates
cat ~/.cache/wal/ghostty.conf # Check ghostty colors
```
3. **Manual theme application:**
```bash
~/.local/bin/apply-theme.sh --light
# Check: GTK apps, Qt apps, Ghostty colors update
~/.local/bin/apply-theme.sh --dark
# Check: Theme switches to dark
```
4. **Verify theme files:**
- `~/.cache/wal/colors.json` - Generated by pywal
- `~/.cache/wal/gtk-3.0.css` - From custom template
- `~/.cache/wal/gtk-4.0.css` - From custom template
- `~/.cache/wal/kvantum.kvconfig` - From custom template
- `~/.cache/wal/ghostty.conf` - From built-in template
5. **Test Kvantum theme:**
```bash
kvantummanager --set PywalTheme
# Or check: cat ~/.config/Kvantum/kvantum.kvconfig
```
6. **Test systemd automation:**
```bash
systemctl --user start theme-switcher.service
systemctl --user status theme-switcher.timer
systemctl --user list-timers | grep theme
```
7. **Test with live applications:**
- Open GTK app (nautilus, gnome-calculator) - verify colors
- Open Qt app (if available) - verify Kvantum theme
- Open Ghostty terminal - verify color palette matches wallpaper
- Check waybar/rofi if using pywal templates for those
## Implementation Steps Summary
### Phase 1: Setup pywal with existing wallpapers
1. Create theme_switcher module with pywal16 package
2. Test color extraction on Light/ and Dark/ wallpapers
3. Verify built-in templates (ghostty.conf, colors.css) generate correctly
### Phase 2: Create custom templates
1. Create `templates/gtk-3.0.css` using pywal variables
2. Create `templates/gtk-4.0.css` using pywal variables
3. Create `templates/kvantum.kvconfig` mapping colors.json to Kvantum format
4. Install templates to `~/.config/wal/templates/`
### Phase 3: Theme application script
1. Write `apply-theme.sh` script that:
- Runs pywal with appropriate flags (-l for light)
- Symlinks generated CSS to GTK config directories
- Copies Kvantum config to `~/.config/Kvantum/PywalTheme/`
- Runs `kvantummanager --set PywalTheme`
- Updates gsettings for color-scheme preference
### Phase 4: Time-based automation
1. Create systemd timer for light/dark switching times
2. Create systemd service that calls apply-theme.sh
3. Script determines mode based on current time
4. Selects random wallpaper from Light/ or Dark/ directory
### Phase 5: Integration and cleanup
1. Modify existing home.nix files to disable static themes when module enabled
2. Keep icon themes, cursor themes (not wallpaper-dependent)
3. Test on one host before deploying to all
4. Document wpaperd integration approach for future enhancement
## Key Technical Details
**Pywal template syntax:**
```
{background} -> #081212
{foreground} -> #c1c3c3
{color0} -> #081212
...
{color15} -> #c1c3c3
{cursor} -> #c1c3c3
{wallpaper} -> /path/to/wallpaper.jpg
```
**Template location:** `~/.config/wal/templates/filename.ext`
**Output location:** `~/.cache/wal/filename.ext`
**Ghostty:** Automatically reloads config when files change - no manual reload needed
**GTK:** Changes propagate via gsettings/dconf - running apps update automatically
**Kvantum:** May require app restart to see changes (Qt limitation)
**Wallpaper directories:**
- Light: `/home/nate/nixos/shared/modules/services/wallpapers/Light/`
- Dark: `/home/nate/nixos/shared/modules/services/wallpapers/Dark/`

View File

@ -1,447 +0,0 @@
# Dynamic Theme Switcher Module
A NixOS module that automatically generates and applies color themes from wallpapers using pywal16, with integrated wpaperd wallpaper management and support for GTK, Qt/Kvantum, Ghostty, and other applications.
## Features
- **Integrated Wallpaper Management**: Built-in wpaperd integration with configurable rotation modes
- **Automatic Color Extraction**: Uses pywal16 with configurable backends (haishoku, colorthief, etc.) to extract color palettes from wallpapers
- **Multi-Application Support**: Generates themes for GTK 3.0, GTK 4.0, Qt/Kvantum, Ghostty terminal, and more
- **Time-Based Switching**: Automatically switches between light and dark themes based on configured times
- **Flexible Rotation Modes**: Choose between continuous rotation, switch-only, or hybrid modes
- **Manual Control**: Provides scripts for manual theme switching
- **Synchronized Wallpaper & Theme**: Wallpaper and color theme always match
## Directory Structure
```
theme_switcher/
├── default.nix # Main module configuration
├── templates/
│ ├── gtk-3.0.css # GTK 3.0 theme template
│ ├── gtk-4.0.css # GTK 4.0 theme template
│ └── PywalTheme.kvconfig # Kvantum Qt theme template
├── OUTLINE.md # Implementation specification
└── README.md # This file
```
## Installation
### 1. Import the Module
Add the module to your host configuration:
```nix
# In your host's configuration.nix or desktop-configuration.nix
imports = [
../../shared/modules/services/theme_switcher
];
```
### 2. Enable and Configure
```nix
services.themeSwitcher = {
enable = true;
user = "yourUsername";
# Optional: Customize settings
backend = "haishoku"; # Color extraction backend
lightTime = "06:00:00"; # Switch to light theme at 6 AM
darkTime = "18:00:00"; # Switch to dark theme at 6 PM
enableAutoSwitch = true; # Enable automatic time-based switching
# Wallpaper rotation settings
rotation = {
mode = "continuous"; # Options: continuous, switch-only, hybrid
interval = "5m"; # Rotation interval (for continuous mode)
};
# Wpaperd configuration
wpaperd = {
enable = true;
mode = "center"; # Options: center, fit, stretch, tile
transition.effect = "fade";
transition.duration = 300; # milliseconds
};
};
```
**Note:** If you were previously using the separate `wallpaper-rotator` module, you should disable it to avoid conflicts.
### 3. Wallpaper Directory Structure
Ensure your wallpaper directory has Light and Dark subdirectories:
```
wallpapers/
├── Light/
│ ├── wallpaper1.jpg
│ ├── wallpaper2.jpg
│ └── ...
└── Dark/
├── wallpaper1.jpg
├── wallpaper2.jpg
└── ...
```
### 4. Rebuild Your System
```bash
sudo nixos-rebuild switch --flake .#hostname
```
## Configuration Options
### Core Options
#### `services.themeSwitcher.enable`
- **Type**: boolean
- **Default**: false
- **Description**: Enable the dynamic theme switcher with integrated wallpaper management
#### `services.themeSwitcher.user`
- **Type**: string
- **Required**: yes
- **Description**: Username for which to apply theme switching
#### `services.themeSwitcher.wallpaperPath`
- **Type**: string
- **Default**: `${homeDirectory}/nixos/shared/modules/services/wallpapers`
- **Description**: Path to wallpaper directories (must contain Light/ and Dark/ subdirectories)
#### `services.themeSwitcher.backend`
- **Type**: enum
- **Default**: `"haishoku"`
- **Options**: `"haishoku"`, `"modern_colorthief"`, `"fast_colorthief"`, `"colorthief"`, `"colorz"`, `"wal"`
- **Description**: Pywal color extraction backend algorithm
#### `services.themeSwitcher.lightTime`
- **Type**: string
- **Default**: `"06:00:00"`
- **Description**: Time to switch to light theme (HH:MM:SS format)
#### `services.themeSwitcher.darkTime`
- **Type**: string
- **Default**: `"18:00:00"`
- **Description**: Time to switch to dark theme (HH:MM:SS format)
#### `services.themeSwitcher.enableAutoSwitch`
- **Type**: boolean
- **Default**: true
- **Description**: Enable automatic time-based theme switching via systemd timer
### Rotation Options
#### `services.themeSwitcher.rotation.mode`
- **Type**: enum
- **Default**: `"continuous"`
- **Options**:
- `"continuous"`: Rotate wallpapers every interval AND regenerate theme each time
- `"switch-only"`: Only change wallpaper at light/dark switch times (6 AM/6 PM)
- `"hybrid"`: Rotate wallpapers but only regenerate theme at switch times (not yet implemented)
- **Description**: Wallpaper rotation and theme generation behavior
#### `services.themeSwitcher.rotation.interval`
- **Type**: string
- **Default**: `"5m"`
- **Example**: `"30s"`, `"10m"`, `"1h"`
- **Description**: How often to rotate wallpapers in continuous mode
#### `services.themeSwitcher.rotation.sorting`
- **Type**: enum
- **Default**: `"random"`
- **Options**: `"random"`
- **Description**: Wallpaper selection method (currently only random is supported)
### Wpaperd Integration Options
#### `services.themeSwitcher.wpaperd.enable`
- **Type**: boolean
- **Default**: true
- **Description**: Enable wpaperd wallpaper daemon integration
#### `services.themeSwitcher.wpaperd.mode`
- **Type**: enum
- **Default**: `"center"`
- **Options**: `"center"`, `"fit"`, `"fit-border-color"`, `"stretch"`, `"tile"`
- **Description**: How to display wallpapers when size differs from display resolution
#### `services.themeSwitcher.wpaperd.transition.effect`
- **Type**: string
- **Default**: `"fade"`
- **Example**: `"simple"`, `"fade"`
- **Description**: Wallpaper transition effect name
#### `services.themeSwitcher.wpaperd.transition.duration`
- **Type**: integer
- **Default**: 300
- **Description**: Transition duration in milliseconds
## Manual Usage
### Apply Theme Manually
The module installs `apply-theme.sh` to `~/.local/bin/`:
```bash
# Apply light theme with random wallpaper from Light/ directory
~/.local/bin/apply-theme.sh --light
# Apply dark theme with random wallpaper from Dark/ directory
~/.local/bin/apply-theme.sh --dark
# Apply theme with specific wallpaper
~/.local/bin/apply-theme.sh --light --wallpaper-path /path/to/wallpaper.jpg
```
### Check Systemd Timer Status
```bash
# View timer status
systemctl --user status theme-switcher.timer
# List upcoming timer events
systemctl --user list-timers | grep theme
# Manually trigger theme switch
systemctl --user start theme-switcher.service
# View service logs
journalctl --user -u theme-switcher.service
```
### Test Pywal Color Extraction
```bash
# Test light theme generation
wal -i ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg --backend haishoku -l -n
# Test dark theme generation
wal -i ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg --backend haishoku -n
# View generated colors
cat ~/.cache/wal/colors.json
# View generated templates
ls -la ~/.cache/wal/
```
## Generated Files
After running the theme switcher, pywal generates the following files:
### Pywal Cache (`~/.cache/wal/`)
- `colors.json` - Extracted color palette in JSON format
- `colors.sh` - Shell script with color variables
- `ghostty.conf` - Ghostty terminal theme (auto-reloads)
- `gtk-3.0.css` - GTK 3.0 theme (from custom template)
- `gtk-4.0.css` - GTK 4.0 theme (from custom template)
- `PywalTheme.kvconfig` - Kvantum Qt theme (from custom template)
### User Configuration
- `~/.config/gtk-3.0/gtk.css` - Symlink to pywal-generated GTK3 theme
- `~/.config/gtk-4.0/gtk.css` - Symlink to pywal-generated GTK4 theme
- `~/.config/Kvantum/PywalTheme/PywalTheme.kvconfig` - Kvantum theme copy
## Integration with Existing Themes
When enabling this module, you should adjust your existing theme configuration:
### Before (Static Catppuccin Theme)
```nix
gtk = {
enable = true;
theme = {
name = "catppuccin-macchiato-lavender-compact+rimless";
package = pkgs.catppuccin-gtk.override { ... };
};
};
qt = {
enable = true;
platformTheme.name = "kvantum";
style.name = "kvantum";
};
xdg.configFile."Kvantum/kvantum.kvconfig".source = ...;
```
### After (Dynamic Pywal Theme)
```nix
# The theme switcher module automatically sets:
# - gtk.enable = true
# - qt.enable = true
# - qt.platformTheme.name = "kvantum"
# - qt.style.name = "kvantum"
# You can still keep static icon and cursor themes:
gtk.iconTheme = {
name = "Papirus-Dark";
package = pkgs.catppuccin-papirus-folders;
};
gtk.cursorTheme = {
name = "Bibata-Modern-Classic";
package = pkgs.bibata-cursors;
};
```
## Troubleshooting
### Theme Not Applying
```bash
# Check if pywal generated files
ls -la ~/.cache/wal/
# Check systemd service status
systemctl --user status theme-switcher.service
# View detailed logs
journalctl --user -u theme-switcher.service -n 50
# Manually run apply-theme.sh with verbose output
~/.local/bin/apply-theme.sh --light
```
### Kvantum Theme Not Working
```bash
# Check if Kvantum theme was created
ls -la ~/.config/Kvantum/PywalTheme/
# Manually set Kvantum theme
kvantummanager --set PywalTheme
# Check if Qt applications are using Kvantum
echo $QT_STYLE_OVERRIDE # Should be "kvantum"
```
### Colors Don't Match Wallpaper
Try a different pywal backend:
```nix
services.themeSwitcher.backend = "modern_colorthief"; # or "fast_colorthief", "colorthief", etc.
```
### GTK Applications Not Updating
```bash
# Check if GTK CSS files exist
ls -la ~/.config/gtk-3.0/gtk.css
ls -la ~/.config/gtk-4.0/gtk.css
# Restart GTK applications for changes to take effect
```
## Supported Applications
### Automatically Themed
- **GTK 3 Applications**: Nautilus, GNOME Calculator, etc.
- **GTK 4 Applications**: GNOME Text Editor, newer GNOME apps
- **Qt Applications**: KDE apps when using Kvantum
- **Ghostty Terminal**: Auto-reloads on config change
- **Waybar**: If using pywal template (built-in)
- **Rofi**: If using pywal template (built-in)
- **Hyprland/Sway**: If using pywal template (built-in)
### Manual Integration Required
- **Firefox**: Use pywal-firefox extension
- **VSCode**: Use pywal theme extension
- **Other Applications**: Check if pywal templates exist in `~/.cache/wal/`
## Advanced Usage
### Custom Pywal Templates
To add your own templates, create files in `~/.config/wal/templates/`:
```bash
# Template file: ~/.config/wal/templates/myapp.conf
# Use pywal template variables
background = {background}
foreground = {foreground}
color0 = {color0}
...
color15 = {color15}
```
After running `apply-theme.sh`, find your generated file in `~/.cache/wal/myapp.conf`.
### Disable Automatic Switching
To use manual control only:
```nix
services.themeSwitcher = {
enable = true;
user = "username";
enableAutoSwitch = false; # Disable timer
};
```
Then use `~/.local/bin/apply-theme.sh` manually as needed.
### Rotation Modes Explained
#### Continuous Mode (Default)
- Wallpaper rotates every interval (e.g., 5 minutes)
- Theme is regenerated from each new wallpaper
- Wallpaper and theme always match
- **Resource usage**: Moderate (pywal runs every interval)
- **Best for**: Users who want frequently changing, perfectly matched themes
#### Switch-Only Mode
- Wallpaper changes only at light/dark switch times (6 AM / 6 PM)
- Theme is generated once per switch
- Wallpaper and theme always match
- **Resource usage**: Minimal (pywal runs twice per day)
- **Best for**: Users who prefer consistent themes throughout the day
#### Hybrid Mode (Future)
- Wallpaper rotates every interval
- Theme is generated only at switch times
- Wallpaper and theme may not match mid-day
- **Not yet implemented**
### Migration from wallpaper-rotator Module
If you were previously using the separate `wallpaper-rotator` module:
1. **Disable the old module** in your configuration:
```nix
# Comment out or remove:
# services.wallpaperRotator.enable = true;
```
2. **Enable themeSwitcher** with equivalent settings:
```nix
services.themeSwitcher = {
enable = true;
user = "yourUsername";
rotation.interval = "5m"; # Same as old duration setting
wpaperd.mode = "center"; # Same as old mode setting
};
```
3. **Rebuild** your system - wpaperd will now be managed by themeSwitcher
## Technical Details
- **Pywal Template Syntax**: `{background}`, `{foreground}`, `{color0}`-`{color15}`, `{cursor}`, `{wallpaper}`
- **Template Location**: `~/.config/wal/templates/filename.ext`
- **Output Location**: `~/.cache/wal/filename.ext`
- **Color Extraction**: 16-color palette using pywal16
- **GTK Reload**: Automatic via gsettings changes
- **Kvantum Reload**: May require app restart
- **Ghostty Reload**: Automatic on config file change
## Future Enhancements
- Direct wpaperd integration (trigger theme change on wallpaper rotation)
- Per-monitor theme configuration
- Additional application templates (Firefox, VSCode, etc.)
- Theme preview before applying
- Fallback color schemes for monochrome wallpapers
## Credits
- Built for NixOS using home-manager
- Uses [pywal16](https://github.com/eylles/pywal16) for color extraction
- Based on [pywal](https://github.com/dylanaraps/pywal) original concept

View File

@ -1,347 +0,0 @@
# Theme Switcher Testing Guide
This guide will help you test the theme switcher module on the nate-work host.
## Pre-Testing Checklist
### 1. Review Current Configuration
Check your current nate-work configuration:
```bash
# View current wallpaper rotator settings
grep -A 10 "wallpaperRotator" nate-work/desktop-configuration.nix
# View current GTK/Qt theme settings
grep -A 10 "gtk\|kvantum" nate-work/modules/home-manager/home.nix
```
### 2. Files to Modify
You'll need to modify these files:
1. **nate-work/desktop-configuration.nix** (or wherever you import modules)
- Add themeSwitcher import
- Disable wallpaperRotator if enabled
- Configure themeSwitcher settings
2. **nate-work/modules/home-manager/home.nix**
- Comment out static GTK theme configuration
- Comment out static Kvantum configuration
- Keep icon and cursor themes
## Testing Phase 1: Manual Mode
Start with manual mode to test theme generation before enabling automation.
### Step 1: Add Module Configuration
Edit `nate-work/desktop-configuration.nix`:
```nix
{
imports = [
# ... existing imports ...
../../shared/modules/services/theme_switcher
];
# Disable old wallpaper rotator if present
# services.wallpaperRotator.enable = false;
# Enable theme switcher in manual mode
services.themeSwitcher = {
enable = true;
user = "nate";
# Start with manual mode (no auto-switching)
enableAutoSwitch = false;
# Use switch-only mode for testing (no continuous rotation yet)
rotation.mode = "switch-only";
# Configure wpaperd
wpaperd = {
enable = true;
mode = "center";
transition.effect = "fade";
transition.duration = 300;
};
};
}
```
### Step 2: Comment Out Static Themes
Edit `nate-work/modules/home-manager/home.nix`:
```nix
# Comment out these sections:
# gtk.theme = { ... };
# xdg.configFile."Kvantum/kvantum.kvconfig" = ...;
# xdg.configFile."gtk-4.0/..." = ...;
# Keep these (icon and cursor themes are independent):
gtk.iconTheme = { ... }; # Keep
gtk.cursorTheme = { ... }; # Keep
```
### Step 3: Rebuild System
```bash
# From /home/nate/nixos directory
sudo nixos-rebuild switch --flake .#nate-work
```
This will:
- Install pywal16, haishoku, wpaperd
- Install apply-theme.sh to ~/.local/bin/
- Install pywal templates
- Start wpaperd (but with no auto-rotation)
- NOT start any timers (manual mode)
### Step 4: Manual Testing
After rebuild, test the script manually:
```bash
# Test light theme
~/.local/bin/apply-theme.sh --light
# Verify:
# 1. Wallpaper changed to one from Light/ directory
# 2. Colors extracted: cat ~/.cache/wal/colors.json
# 3. GTK theme generated: ls ~/.config/gtk-3.0/gtk.css
# 4. Kvantum theme generated: ls ~/.config/Kvantum/PywalTheme/
# 5. Open a GTK app (nautilus, calculator) - check colors
```
```bash
# Test dark theme
~/.local/bin/apply-theme.sh --dark
# Verify same things with Dark wallpaper
```
```bash
# Test with specific wallpaper
~/.local/bin/apply-theme.sh --light --wallpaper-path ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg
```
### Step 5: Verify Wpaperd Integration
```bash
# Check if wpaperd is running
pgrep wpaperd
# Check wpaperd status
systemctl --user status wpaperd
# Manually change wallpaper using wpaperctl
wpaperctl wallpaper ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg
```
## Testing Phase 2: Automatic Switching (Time-Based)
Once manual testing works, enable automatic time-based switching.
### Step 1: Enable Auto-Switch
Edit `nate-work/desktop-configuration.nix`:
```nix
services.themeSwitcher = {
enable = true;
user = "nate";
# Enable automatic switching
enableAutoSwitch = true;
# Keep switch-only mode for now
rotation.mode = "switch-only";
# Optional: Adjust times for testing
lightTime = "08:00:00"; # Your preferred time
darkTime = "17:00:00"; # Your preferred time
};
```
### Step 2: Rebuild and Check Timer
```bash
sudo nixos-rebuild switch --flake .#nate-work
# Verify timer is installed
systemctl --user list-timers | grep theme
# Should show two entries (one for lightTime, one for darkTime)
systemctl --user status theme-switcher.timer
```
### Step 3: Test Timer Manually
```bash
# Manually trigger the service
systemctl --user start theme-switcher.service
# Check logs
journalctl --user -u theme-switcher.service -n 50
# Should show theme being applied based on current time
```
## Testing Phase 3: Continuous Rotation
Once automatic switching works, enable continuous wallpaper rotation.
### Step 1: Enable Continuous Mode
Edit `nate-work/desktop-configuration.nix`:
```nix
services.themeSwitcher = {
enable = true;
user = "nate";
enableAutoSwitch = true;
# Enable continuous rotation
rotation = {
mode = "continuous";
interval = "5m"; # Rotate every 5 minutes
};
};
```
### Step 2: Rebuild and Monitor
```bash
sudo nixos-rebuild switch --flake .#nate-work
# Check rotation timer
systemctl --user list-timers | grep wallpaper
systemctl --user status wallpaper-rotation.timer
# Watch logs in real-time
journalctl --user -u wallpaper-rotation.service -f
```
### Step 3: Verify Rotation
Wait 5 minutes and verify:
- Wallpaper changes
- Theme regenerates
- Colors match new wallpaper
## Troubleshooting
### Theme Not Applying
```bash
# Check if pywal generated files
ls -la ~/.cache/wal/
# Expected files:
# - colors.json
# - gtk-3.0.css
# - gtk-4.0.css
# - PywalTheme.kvconfig
# - ghostty.conf
```
### Wallpaper Not Changing
```bash
# Check wpaperd status
systemctl --user status wpaperd
journalctl --user -u wpaperd -n 50
# Try manual change
wpaperctl wallpaper ~/path/to/wallpaper.jpg
```
### GTK Theme Not Loading
```bash
# Check GTK config
ls -la ~/.config/gtk-3.0/
ls -la ~/.config/gtk-4.0/
# Check if files are symlinks
readlink ~/.config/gtk-3.0/gtk.css
# Should point to ~/.cache/wal/gtk-3.0.css
```
### Kvantum Theme Not Loading
```bash
# Check Kvantum directory
ls -la ~/.config/Kvantum/PywalTheme/
# Manually set theme
kvantummanager --set PywalTheme
# Restart Qt applications
```
### Service Failures
```bash
# Check service logs
journalctl --user -u theme-switcher.service --since today
journalctl --user -u wallpaper-rotation.service --since today
# Check environment
systemctl --user show theme-switcher.service | grep PATH
```
## Performance Monitoring
### Resource Usage (Continuous Mode)
```bash
# Monitor theme regeneration
watch -n 60 'ls -lh ~/.cache/wal/colors.json'
# Check pywal CPU usage
top -b -n 1 | grep wal
```
If continuous mode is too resource-intensive, switch to switch-only mode.
## Success Criteria
- ✓ Manual theme switching works (--light and --dark)
- ✓ Wallpapers change via wpaperctl
- ✓ GTK apps reflect new colors
- ✓ Qt apps reflect new Kvantum theme
- ✓ Ghostty terminal shows new colors (if installed)
- ✓ Timers appear in systemctl --user list-timers
- ✓ Automatic switching works at configured times
- ✓ Continuous rotation works (if enabled)
- ✓ No service failures in journalctl
## Next Steps After Testing
Once everything works on nate-work:
1. Consider applying to other hosts (frame12, nate, scrappy)
2. Fine-tune rotation interval if needed
3. Try different pywal backends (modern_colorthief, etc.)
4. Customize wpaperd transition effects
5. Add custom pywal templates for other apps
## Rollback Plan
If something goes wrong:
```bash
# Disable the module
# In nate-work/desktop-configuration.nix:
services.themeSwitcher.enable = false;
# Re-enable old configuration
# services.wallpaperRotator.enable = true;
# Uncomment GTK/Qt theme settings in home.nix
# Rebuild
sudo nixos-rebuild switch --flake .#nate-work
```

View File

@ -1,243 +0,0 @@
#!/usr/bin/env python3
"""
Apply Themes Script - Applies GTK/Kvantum/Helix themes after pywal runs.
This script:
1. Links GTK CSS files from pywal cache
2. Sets GTK color scheme preference based on light/dark mode
3. Copies and applies Kvantum theme
4. Copies Helix theme to config directory
5. Optionally runs color_mapper.py for semantic color mapping
"""
import json
import shutil
import subprocess
import sys
from pathlib import Path
import logging
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
class ThemeApplicator:
"""Applies themes after pywal has generated color palette."""
def __init__(self):
self.home = Path.home()
self.cache_dir = self.home / ".cache/wal"
self.config_dir = self.home / ".config"
def is_light_mode(self) -> bool:
"""
Determine if current theme is light mode by checking pywal cache.
Returns:
True if light mode, False if dark mode
"""
colors_json = self.cache_dir / "colors.json"
if not colors_json.exists():
logger.warning("colors.json not found, assuming dark mode")
return False
try:
with open(colors_json) as f:
data = json.load(f)
return data.get("special", {}).get("light", False)
except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"Failed to parse colors.json: {e}, assuming dark mode")
return False
def setup_gtk_themes(self) -> None:
"""Link GTK CSS files from pywal cache."""
gtk3_dir = self.config_dir / "gtk-3.0"
gtk4_dir = self.config_dir / "gtk-4.0"
# Ensure directories exist
gtk3_dir.mkdir(parents=True, exist_ok=True)
gtk4_dir.mkdir(parents=True, exist_ok=True)
# Link GTK 3.0 CSS
gtk3_source = self.cache_dir / "gtk-3.0.css"
gtk3_target = gtk3_dir / "gtk.css"
if gtk3_source.exists():
# Remove old symlink/file
if gtk3_target.exists() or gtk3_target.is_symlink():
gtk3_target.unlink()
gtk3_target.symlink_to(gtk3_source)
logger.info(f"Linked GTK 3.0 theme: {gtk3_target}")
else:
logger.warning(f"GTK 3.0 CSS not found: {gtk3_source}")
# Link GTK 4.0 CSS
gtk4_source = self.cache_dir / "gtk-4.0.css"
gtk4_target = gtk4_dir / "gtk.css"
if gtk4_source.exists():
# Remove old symlink/file
if gtk4_target.exists() or gtk4_target.is_symlink():
gtk4_target.unlink()
gtk4_target.symlink_to(gtk4_source)
logger.info(f"Linked GTK 4.0 theme: {gtk4_target}")
else:
logger.warning(f"GTK 4.0 CSS not found: {gtk4_source}")
def set_gtk_color_scheme(self, is_light: bool) -> None:
"""
Set GTK color scheme preference using gsettings.
Args:
is_light: True for light mode, False for dark mode
"""
scheme = "prefer-light" if is_light else "prefer-dark"
try:
subprocess.run(
['gsettings', 'set', 'org.gnome.desktop.interface', 'color-scheme', scheme],
check=True,
timeout=5,
capture_output=True
)
logger.info(f"Set GTK color scheme to: {scheme}")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
logger.warning(f"Failed to set GTK color scheme (this is normal if not using GNOME): {e}")
def setup_kvantum_theme(self) -> None:
"""Copy and apply Kvantum theme from pywal cache."""
kvantum_dir = self.config_dir / "Kvantum/PywalTheme"
kvantum_dir.mkdir(parents=True, exist_ok=True)
# Copy Kvantum config
kvantum_source = self.cache_dir / "PywalTheme.kvconfig"
kvantum_target = kvantum_dir / "PywalTheme.kvconfig"
if kvantum_source.exists():
shutil.copy2(kvantum_source, kvantum_target)
logger.info(f"Copied Kvantum theme: {kvantum_target}")
# Apply Kvantum theme
try:
subprocess.run(
['kvantummanager', '--set', 'PywalTheme'],
check=True,
timeout=5,
capture_output=True
)
logger.info("Applied Kvantum theme: PywalTheme")
except FileNotFoundError:
logger.warning("kvantummanager not found, skipping Kvantum theme application")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
logger.warning(f"Failed to apply Kvantum theme: {e}")
else:
logger.warning(f"Kvantum theme not found: {kvantum_source}")
def setup_helix_theme(self) -> None:
"""Copy Helix theme from pywal cache to config directory."""
helix_themes_dir = self.config_dir / "helix/themes"
helix_themes_dir.mkdir(parents=True, exist_ok=True)
# Copy minimal Helix theme
helix_source = self.cache_dir / "helix_minimal.toml"
helix_target = helix_themes_dir / "pywal_minimal.toml"
if helix_source.exists():
shutil.copy2(helix_source, helix_target)
logger.info(f"Updated Helix theme: pywal_minimal")
else:
logger.warning(f"Helix theme not found: {helix_source}")
# Copy semantic Helix theme if it exists
helix_semantic_source = self.cache_dir / "helix_semantic.toml"
helix_semantic_target = helix_themes_dir / "pywal_semantic.toml"
if helix_semantic_source.exists():
shutil.copy2(helix_semantic_source, helix_semantic_target)
logger.info(f"Updated Helix semantic theme: pywal_semantic")
def run_color_mapper(self) -> None:
"""
Run color_mapper.py to generate semantic color mappings.
This is optional and will only run if the script exists.
"""
# Look for color_mapper.py in the same directory as this script
script_dir = Path(__file__).parent
color_mapper = script_dir / "color_mapper.py"
if not color_mapper.exists():
logger.debug(f"color_mapper.py not found at {color_mapper}, skipping")
return
try:
result = subprocess.run(
['python3', str(color_mapper)],
capture_output=True,
text=True,
timeout=10,
check=True
)
logger.info("Applied semantic color mapping")
# Log color mapper output for debugging
if result.stdout:
for line in result.stdout.strip().split('\n'):
logger.debug(f"color_mapper: {line}")
except FileNotFoundError:
logger.warning("python3 not found, skipping color mapper")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
logger.warning(f"Failed to run color_mapper.py: {e}")
def run(self) -> int:
"""
Execute theme application process.
Returns:
Exit code (0 for success, 1 for failure)
"""
try:
# Check if pywal has been run
if not self.cache_dir.exists():
logger.error(f"Pywal cache not found: {self.cache_dir}")
logger.error("Please run pywal first")
return 1
# Determine mode
is_light = self.is_light_mode()
mode = "light" if is_light else "dark"
logger.info(f"Applying {mode} mode themes")
# Setup GTK themes
self.setup_gtk_themes()
self.set_gtk_color_scheme(is_light)
# Setup Kvantum theme
self.setup_kvantum_theme()
# Setup Helix theme
self.setup_helix_theme()
# Run color mapper for semantic themes
self.run_color_mapper()
logger.info("All themes applied successfully")
return 0
except Exception as e:
logger.error(f"Theme application failed: {e}", exc_info=True)
return 1
def main():
"""Main entry point."""
applicator = ThemeApplicator()
return applicator.run()
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,304 +0,0 @@
#!/usr/bin/env python3
"""
Intelligent color mapper for pywal themes.
Analyzes generated pywal colors and maps them to semantic roles based on
hue, saturation, and lightness. Creates a mapping file that can be used
to apply consistent semantic meaning to colors regardless of wallpaper.
"""
import json
import colorsys
from pathlib import Path
from typing import Dict, Tuple, List
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
"""Convert hex color to RGB tuple."""
hex_color = hex_color.lstrip('#')
return (int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16))
def rgb_to_hsl(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
"""Convert RGB to HSL."""
r, g, b = [x / 255.0 for x in rgb]
h, l, s = colorsys.rgb_to_hls(r, g, b)
return (h * 360, s * 100, l * 100) # Convert to degrees and percentages
def analyze_color(hex_color: str) -> Dict:
"""Analyze a color and return its properties."""
rgb = hex_to_rgb(hex_color)
h, s, l = rgb_to_hsl(rgb)
# Determine color category based on hue
if s < 10:
category = "neutral"
elif 0 <= h < 30 or 330 <= h <= 360:
category = "red"
elif 30 <= h < 60:
category = "orange"
elif 60 <= h < 90:
category = "yellow"
elif 90 <= h < 150:
category = "green"
elif 150 <= h < 210:
category = "cyan"
elif 210 <= h < 270:
category = "blue"
elif 270 <= h < 330:
category = "magenta"
else:
category = "neutral"
return {
"hex": hex_color,
"rgb": rgb,
"hue": h,
"saturation": s,
"lightness": l,
"category": category
}
def calculate_contrast(color1_hsl: Tuple[float, float, float], color2_hsl: Tuple[float, float, float]) -> float:
"""Calculate perceived contrast between two colors based on lightness difference."""
return abs(color1_hsl[2] - color2_hsl[2])
def find_best_color_for_role(colors: Dict[str, Dict], role: str, is_light_mode: bool, bg_lightness: float = 50.0) -> str:
"""
Find the best color from the palette for a given semantic role.
Roles: error, warning, success, info, emphasis, secondary
"""
# Define preferred characteristics for each role
role_preferences = {
"error": {
"categories": ["red", "orange"],
"min_saturation": 30,
"prefer_vibrant": True,
"prefer_contrast": True # Errors need high visibility
},
"warning": {
"categories": ["orange", "yellow"],
"min_saturation": 30,
"prefer_vibrant": True,
"prefer_contrast": True
},
"comment": {
"categories": ["yellow", "orange", "green", "cyan"], # Flexible for high contrast
"min_saturation": 25,
"prefer_vibrant": False,
"prefer_contrast": True # Comments should be very visible
},
"string": {
"categories": ["green", "cyan"],
"min_saturation": 25,
"prefer_vibrant": False,
"prefer_contrast": False
},
"keyword": {
"categories": ["magenta", "blue"],
"min_saturation": 20,
"prefer_vibrant": False,
"prefer_contrast": False
},
"number": {
"categories": ["orange", "red", "magenta"],
"min_saturation": 30,
"prefer_vibrant": True,
"prefer_contrast": False
},
"info": {
"categories": ["blue", "cyan"],
"min_saturation": 20,
"prefer_vibrant": False,
"prefer_contrast": False
}
}
prefs = role_preferences.get(role, {})
preferred_cats = prefs.get("categories", [])
min_sat = prefs.get("min_saturation", 0)
prefer_vibrant = prefs.get("prefer_vibrant", False)
prefer_contrast = prefs.get("prefer_contrast", False)
# Score each color
best_score = -1
best_color = None
for color_name, props in colors.items():
if props["category"] == "neutral":
continue
score = 0
# Category match (highest priority)
if props["category"] in preferred_cats:
score += 100
# Saturation requirements
if props["saturation"] >= min_sat:
score += 50
# Contrast preference (for comments and errors)
if prefer_contrast:
# Calculate contrast with background
contrast = abs(props["lightness"] - bg_lightness)
score += contrast * 0.5 # Add up to 50 points for maximum contrast
# Vibrancy preference
if prefer_vibrant:
# For vibrant colors, prefer higher saturation and medium lightness
score += props["saturation"] / 2
if 40 <= props["lightness"] <= 60:
score += 20
else:
# For less vibrant, prefer moderate saturation
if 30 <= props["saturation"] <= 70:
score += 20
# Light mode adjustments - prefer colors with good contrast
if is_light_mode:
# In light mode, prefer darker, more saturated colors for visibility
if props["lightness"] < 50:
score += 10
else:
# In dark mode, prefer brighter colors
if props["lightness"] > 40:
score += 10
if score > best_score:
best_score = score
best_color = color_name
# Fallback to first color if no match found
if best_color is None:
best_color = list(colors.keys())[0]
return best_color
def create_semantic_mapping(colors_json_path: Path) -> Dict:
"""Create semantic color mapping from pywal colors."""
with open(colors_json_path) as f:
data = json.load(f)
# Determine if light mode and get background lightness
bg_hex = data.get("special", {}).get("background", "#000000")
bg_rgb = hex_to_rgb(bg_hex)
bg_hsl = rgb_to_hsl(bg_rgb)
bg_lightness = bg_hsl[2]
is_light_mode = bg_lightness > 50
# Analyze all colors
analyzed = {}
for i in range(1, 8): # color1 through color7 (skip color0 which is usually bg)
color_key = f"color{i}"
hex_color = data["colors"][color_key]
analyzed[color_key] = analyze_color(hex_color)
# Map semantic roles to best matching colors
mapping = {
"error": find_best_color_for_role(analyzed, "error", is_light_mode, bg_lightness),
"warning": find_best_color_for_role(analyzed, "warning", is_light_mode, bg_lightness),
"comment": find_best_color_for_role(analyzed, "comment", is_light_mode, bg_lightness),
"string": find_best_color_for_role(analyzed, "string", is_light_mode, bg_lightness),
"keyword": find_best_color_for_role(analyzed, "keyword", is_light_mode, bg_lightness),
"number": find_best_color_for_role(analyzed, "number", is_light_mode, bg_lightness),
"info": find_best_color_for_role(analyzed, "info", is_light_mode, bg_lightness),
}
# Add the original color values for reference
result = {
"semantic_mapping": mapping,
"is_light_mode": is_light_mode,
"color_analysis": analyzed
}
return result
def apply_semantic_mapping(template_path: Path, output_path: Path, mapping: Dict):
"""Apply semantic mapping to Helix template."""
with open(template_path) as f:
template = f.read()
# Read pywal colors
colors_json = Path.home() / ".cache/wal/colors.json"
with open(colors_json) as f:
colors_data = json.load(f)
# Replace color references with semantically mapped ones
semantic = mapping["semantic_mapping"]
# First, do standard pywal substitutions
output = template
# Replace special colors
for key, value in colors_data["special"].items():
output = output.replace(f"{{{key}}}", value)
# Replace palette colors
for key, value in colors_data["colors"].items():
output = output.replace(f"{{{key}}}", value)
# Now apply semantic color replacements
error_color = colors_data["colors"][semantic["error"]]
warning_color = colors_data["colors"][semantic["warning"]]
comment_color = colors_data["colors"][semantic["comment"]]
string_color = colors_data["colors"][semantic["string"]]
keyword_color = colors_data["colors"][semantic["keyword"]]
number_color = colors_data["colors"][semantic["number"]]
info_color = colors_data["colors"][semantic["info"]]
# Replace semantic placeholders
output = output.replace("{{ERROR_COLOR}}", error_color)
output = output.replace("{{WARNING_COLOR}}", warning_color)
output = output.replace("{{COMMENT_COLOR}}", comment_color)
output = output.replace("{{STRING_COLOR}}", string_color)
output = output.replace("{{KEYWORD_COLOR}}", keyword_color)
output = output.replace("{{NUMBER_COLOR}}", number_color)
output = output.replace("{{INFO_COLOR}}", info_color)
with open(output_path, 'w') as f:
f.write(output)
def main():
"""Main entry point."""
import sys
colors_json = Path.home() / ".cache/wal/colors.json"
if not colors_json.exists():
print("Error: Pywal colors.json not found. Run pywal first.")
return 1
# Analyze colors and create mapping
mapping = create_semantic_mapping(colors_json)
# Save mapping for debugging/inspection
mapping_output = Path.home() / ".cache/wal/semantic_mapping.json"
with open(mapping_output, 'w') as f:
json.dump(mapping, f, indent=2)
print("Semantic color mapping created:")
for role, color in mapping["semantic_mapping"].items():
props = mapping["color_analysis"][color]
print(f" {role:12} -> {color} ({props['category']:8} {props['hex']})")
# Apply to helix template if it exists
helix_template = Path.home() / ".config/wal/templates/helix_semantic.toml"
if helix_template.exists():
helix_output = Path.home() / ".cache/wal/helix_semantic.toml"
apply_semantic_mapping(helix_template, helix_output, mapping)
print(f"\nApplied semantic mapping to: {helix_output}")
# Also copy to helix themes directory
helix_themes_dir = Path.home() / ".config/helix/themes"
helix_themes_dir.mkdir(parents=True, exist_ok=True)
final_output = helix_themes_dir / "pywal_semantic.toml"
apply_semantic_mapping(helix_template, final_output, mapping)
print(f"Copied to: {final_output}")
return 0
if __name__ == "__main__":
exit(main())

View File

@ -1,348 +0,0 @@
{ 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;
};
};
}

View File

@ -1,250 +0,0 @@
#!/usr/bin/env python3
"""
Simple Helix Theme Tester - Generate Helix theme from image for testing.
Usage:
./helix_theme_tester.py test image.jpg
./helix_theme_tester.py test image.jpg --light
./helix_theme_tester.py test image.jpg --output custom_name.toml
"""
import argparse
import subprocess
import sys
from pathlib import Path
import json
# Import from existing color_mapper
from color_mapper import create_semantic_mapping, apply_semantic_mapping
def generate_palette_from_image(image_path: Path, is_light: bool = False) -> bool:
"""Run pywal on image to generate colors.json"""
cmd = ['wal', '-i', str(image_path), '--backend', 'haishoku', '-n']
if is_light:
cmd.append('-l')
try:
subprocess.run(cmd, check=True, timeout=30)
print(f"✓ Generated palette from {image_path.name}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to generate palette: {e}")
return False
except FileNotFoundError:
print("✗ Error: 'wal' command not found. Is pywal16 installed?")
return False
def generate_helix_theme(output_name: str = "pywal_test") -> bool:
"""Generate Helix theme using color_mapper logic"""
colors_json = Path.home() / ".cache/wal/colors.json"
if not colors_json.exists():
print(f"✗ colors.json not found at {colors_json}")
return False
# Use existing color_mapper to create semantic mapping
print("✓ Analyzing colors and creating semantic mapping...")
mapping = create_semantic_mapping(colors_json)
# Print the mapping for visibility
print("\nSemantic color mapping:")
for role, color in mapping["semantic_mapping"].items():
props = mapping["color_analysis"][color]
print(f" {role:12} -> {color} ({props['category']:8} {props['hex']})")
# Check for contrast issues
print("\nContrast analysis:")
bg_lightness = mapping.get("color_analysis", {}).get("color0", {}).get("lightness", 50)
for role, color_key in mapping["semantic_mapping"].items():
color_props = mapping["color_analysis"][color_key]
contrast = abs(color_props["lightness"] - bg_lightness)
status = "" if contrast > 30 else ""
print(f" {status} {role:12}: contrast {contrast:.1f}")
# Apply to helix template
helix_template = Path(__file__).parent / "templates/helix_semantic.toml"
helix_output = Path.home() / f".config/helix/themes/{output_name}.toml"
helix_output.parent.mkdir(parents=True, exist_ok=True)
apply_semantic_mapping(helix_template, helix_output, mapping)
print(f"\n✓ Helix theme created: {helix_output}")
print(f"\nTest in Helix:")
print(f" hx some_file.py")
print(f" :theme {output_name}")
return True
def main():
parser = argparse.ArgumentParser(
description='Generate Helix theme from image for testing'
)
parser.add_argument('command', choices=['test'], help='Command to run')
parser.add_argument('image', type=Path, help='Path to image file')
parser.add_argument('--light', action='store_true', help='Generate light theme')
parser.add_argument('--output', default='pywal_test', help='Output theme name')
args = parser.parse_args()
if not args.image.exists():
print(f"✗ Image not found: {args.image}")
return 1
print(f"Testing with image: {args.image}")
print(f"Mode: {'light' if args.light else 'dark'}\n")
# Step 1: Generate palette
if not generate_palette_from_image(args.image, args.light):
return 1
# Step 2: Generate Helix theme
if not generate_helix_theme(args.output):
return 1
print("\n✓ Done! Now test the theme in Helix and iterate.")
return 0
if __name__ == '__main__':
sys.exit(main())
Enhanced color_mapper.py with Tweaking
Let me show you what minimal enhancements to add for tweaking:
# Add to color_mapper.py after existing imports
def adjust_color_lightness(hex_color: str, factor: float) -> str:
"""
Adjust color lightness by factor.
factor > 1.0 = lighter, factor < 1.0 = darker
"""
rgb = hex_to_rgb(hex_color)
h, s, l = rgb_to_hsl(rgb)
# Adjust lightness
l = max(0, min(100, l * factor))
# Convert back to RGB then hex
r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100)
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
def adjust_color_saturation(hex_color: str, factor: float) -> str:
"""
Adjust color saturation by factor.
factor > 1.0 = more saturated, factor < 1.0 = less saturated
"""
rgb = hex_to_rgb(hex_color)
h, s, l = rgb_to_hsl(rgb)
# Adjust saturation
s = max(0, min(100, s * factor))
# Convert back
r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100)
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
def calculate_contrast_ratio(color1_hex: str, color2_hex: str) -> float:
"""Calculate WCAG contrast ratio between two colors"""
def relative_luminance(hex_color):
rgb = hex_to_rgb(hex_color)
r, g, b = [x / 255.0 for x in rgb]
# Apply gamma correction
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
return 0.2126 * r + 0.7152 * g + 0.0722 * b
l1 = relative_luminance(color1_hex)
l2 = relative_luminance(color2_hex)
lighter = max(l1, l2)
darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
def enforce_minimum_contrast(fg_hex: str, bg_hex: str, min_ratio: float = 4.5) -> str:
"""
Adjust foreground color to meet minimum contrast ratio with background.
Preserves hue and saturation, only adjusts lightness.
"""
current_ratio = calculate_contrast_ratio(fg_hex, bg_hex)
if current_ratio >= min_ratio:
return fg_hex # Already meets requirement
# Determine if we need to lighten or darken
bg_rgb = hex_to_rgb(bg_hex)
bg_h, bg_s, bg_l = rgb_to_hsl(bg_rgb)
fg_rgb = hex_to_rgb(fg_hex)
fg_h, fg_s, fg_l = rgb_to_hsl(fg_rgb)
# If background is dark, lighten foreground; if light, darken foreground
step = 5 if bg_l < 50 else -5
# Iteratively adjust lightness
attempts = 0
while attempts < 20: # Prevent infinite loop
fg_l += step
fg_l = max(0, min(100, fg_l))
# Convert back to hex
r, g, b = colorsys.hls_to_rgb(fg_h/360, fg_l/100, fg_s/100)
adjusted_hex = f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
if calculate_contrast_ratio(adjusted_hex, bg_hex) >= min_ratio:
return adjusted_hex
attempts += 1
# If we couldn't meet contrast, return best effort
return adjusted_hex
# Add parameters to apply_semantic_mapping
def apply_semantic_mapping(template_path: Path, output_path: Path, mapping: Dict,
adjustments: Dict = None):
"""
Apply semantic mapping to Helix template.
adjustments: Optional dict with tweaking parameters:
{
'comment': {'contrast': 4.5, 'saturation': 1.2},
'error': {'saturation': 1.3, 'force_hue': (0, 30)},
}
"""
with open(template_path) as f:
template = f.read()
# Read pywal colors
colors_json = Path.home() / ".cache/wal/colors.json"
with open(colors_json) as f:
colors_data = json.load(f)
# Get background for contrast calculations
bg_color = colors_data["special"]["background"]
# Replace standard pywal colors first
output = template
for key, value in colors_data["special"].items():
output = output.replace(f"{{{key}}}", value)
for key, value in colors_data["colors"].items():
output = output.replace(f"{{{key}}}", value)
# Now apply semantic colors with optional adjustments
semantic = mapping["semantic_mapping"]
for role in ["error", "warning", "comment", "string", "keyword", "number", "info"]:
color_key = semantic[role]
color_hex = colors_data["colors"][color_key]
# Apply adjustments if provided
if adjustments and role in adjustments:
adj = adjustments[role]
# Apply saturation adjustment
if 'saturation' in adj:
color_hex = adjust_color_saturation(color_hex, adj['saturation'])
# Apply lightness adjustment
if 'lightness' in adj:
color_hex = adjust_color_lightness(color_hex, adj['lightness'])
# Enforce minimum contrast
if 'contrast' in adj:
color_hex = enforce_minimum_contrast(color_hex, bg_color, adj['contrast'])
# Replace in template
placeholder = f"{{{role.upper()}_COLOR}}"
output = output.replace(placeholder, color_hex)
with open(output_path, 'w') as f:
f.write(output)

View File

@ -1,360 +0,0 @@
[%General]
author=Pywal Theme Generator
comment=A Kvantum theme generated from wallpaper colors
x11drag=menubar_and_primary_toolbar
alt_mnemonic=true
left_tabs=false
attach_active_tab=false
mirror_doc_tabs=true
group_toolbar_buttons=false
spread_progressbar=true
composite=true
menu_shadow_depth=6
tooltip_shadow_depth=4
scroll_width=12
scroll_arrows=false
scroll_min_extent=40
slider_width=6
slider_handle_width=20
slider_handle_length=20
center_toolbar_handle=true
check_size=16
textless_progressbar=false
progressbar_thickness=4
menubar_mouse_tracking=true
toolbutton_style=0
double_click=false
translucent_windows=false
blurring=false
popup_blurring=false
opaque=kaffeine,kmplayer,subtitlecomposer,kdenlive,vlc,smplayer,smplayer2,avidemux,avidemux2_qt4,avidemux3_qt4,avidemux3_qt5,kamoso,QtCreator,VirtualBox,VirtualBoxVM,trojita,dragon,digikam,lyx,calligra,ink
vertical_spin_indicators=false
spin_button_width=16
fill_rubberband=false
merge_menubar_with_toolbar=true
small_icon_size=16
large_icon_size=32
button_icon_size=16
toolbar_icon_size=22
combo_as_lineedit=true
hide_combo_checkboxes=true
combo_menu=true
hide_spin_checkboxes=true
button_contents_shift=false
groupbox_top_label=true
inline_spin_indicators=true
joined_inactive_tabs=false
[GeneralColors]
window.color={background}
base.color={background}
alt.base.color={color0}
button.color={color0}
light.color={color8}
mid.light.color={color8}
dark.color={color0}
mid.color={color8}
highlight.color={color4}
inactive.highlight.color={color8}
text.color={foreground}
window.text.color={foreground}
button.text.color={foreground}
disabled.text.color={color8}
tooltip.base.color={color0}
tooltip.text.color={foreground}
highlight.text.color={background}
link.color={color4}
link.visited.color={color5}
progress.indicator.text.color={background}
[Hacks]
transparent_ktitle_label=true
transparent_dolphin_view=true
transparent_pcmanfm_sidepane=true
blur_translucent=false
transparent_menutitle=true
respect_darkness=true
kcapacitybar_as_progressbar=true
force_size_grip=true
iconless_pushbutton=false
iconless_menu=false
disabled_icon_opacity=70
lxqtmainmenu_iconsize=22
normal_default_pushbutton=true
single_top_toolbar=true
tint_on_mouseover=0
transparent_pcmanfm_view=false
no_selection_tint=false
[PanelButtonCommand]
frame=true
frame.element=button
frame.top=4
frame.bottom=4
frame.left=4
frame.right=4
interior=true
interior.element=button
indicator.size=10
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
text.toggle.color={background}
text.shadow=0
text.margin=1
text.iconspacing=4
indicator.element=arrow
text.margin.top=2
text.margin.bottom=2
text.margin.left=2
text.margin.right=2
min_width=+0.3font
min_height=+0.3font
frame.expansion=0
[PanelButtonTool]
inherits=PanelButtonCommand
[Toolbar]
inherits=PanelButtonCommand
interior.element=none
frame.element=none
text.normal.color={foreground}
text.focus.color={foreground}
[ToolbarButton]
inherits=PanelButtonCommand
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
[DockTitle]
inherits=PanelButtonCommand
frame=false
interior=false
text.normal.color={foreground}
text.focus.color={background}
[IndicatorSpinBox]
inherits=PanelButtonCommand
indicator.element=arrow
indicator.size=10
[RadioButton]
inherits=PanelButtonCommand
frame=false
interior.element=radio
text.normal.color={foreground}
text.focus.color={foreground}
[CheckBox]
inherits=PanelButtonCommand
frame=false
interior.element=checkbox
text.normal.color={foreground}
text.focus.color={foreground}
[GenericFrame]
inherits=PanelButtonCommand
frame=true
interior=false
frame.element=common
interior.element=common
frame.top=1
frame.bottom=1
frame.left=1
frame.right=1
[LineEdit]
inherits=PanelButtonCommand
frame.element=lineedit
interior.element=lineedit
text.normal.color={foreground}
text.focus.color={foreground}
[DropDownButton]
inherits=PanelButtonCommand
indicator.element=arrow-down
[IndicatorArrow]
indicator.element=arrow
indicator.size=10
[ToolboxTab]
inherits=PanelButtonCommand
text.normal.color={foreground}
text.press.color={background}
text.focus.color={background}
[Tab]
inherits=PanelButtonCommand
interior.element=tab
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
frame.element=tab
indicator.element=tab
indicator.size=10
frame.top=4
frame.bottom=4
frame.left=4
frame.right=4
[TabFrame]
inherits=PanelButtonCommand
frame.element=tabframe
interior.element=tabframe
frame.top=2
frame.bottom=2
frame.left=2
frame.right=2
[TreeExpander]
inherits=PanelButtonCommand
indicator.size=10
indicator.element=tree
[HeaderSection]
inherits=PanelButtonCommand
interior.element=header
frame.element=header
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
text.toggle.color={background}
frame.top=1
frame.bottom=1
frame.left=1
frame.right=1
[SizeGrip]
indicator.element=resize-grip
[Scrollbar]
inherits=PanelButtonCommand
indicator.element=arrow
indicator.size=10
[ScrollbarSlider]
inherits=PanelButtonCommand
frame.element=scrollbarslider
interior=false
frame.left=6
frame.right=6
frame.top=6
frame.bottom=6
indicator.element=grip
indicator.size=13
[ScrollbarGroove]
inherits=PanelButtonCommand
interior=false
frame=false
[Progressbar]
inherits=PanelButtonCommand
frame.element=progress
interior.element=progress
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
text.toggle.color={background}
text.bold=false
frame.expansion=8
[ProgressbarContents]
inherits=PanelButtonCommand
frame=true
frame.element=progress-pattern
interior.element=progress-pattern
[ItemView]
inherits=PanelButtonCommand
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
text.toggle.color={background}
frame.element=itemview
interior.element=itemview
frame.top=2
frame.bottom=2
frame.left=2
frame.right=2
[Splitter]
indicator.size=48
[Menu]
inherits=PanelButtonCommand
frame.top=3
frame.bottom=3
frame.left=3
frame.right=3
frame.element=menu
interior.element=menu
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
[MenuItem]
inherits=PanelButtonCommand
frame=true
frame.element=menuitem
interior.element=menuitem
indicator.element=menuitem
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
text.toggle.color={background}
frame.top=3
frame.bottom=3
frame.left=3
frame.right=3
[MenuBar]
inherits=PanelButtonCommand
frame.element=menubar
interior.element=menubar
frame.bottom=0
[MenuBarItem]
inherits=PanelButtonCommand
interior=true
interior.element=menubaritem
frame.element=menubaritem
text.normal.color={foreground}
text.focus.color={background}
text.press.color={background}
frame.top=2
frame.bottom=2
frame.left=2
frame.right=2
[TitleBar]
inherits=PanelButtonCommand
frame=false
interior.element=titlebar
indicator.size=16
indicator.element=mdi
text.normal.color={foreground}
text.focus.color={background}
text.bold=true
[ComboBox]
inherits=PanelButtonCommand
interior.element=combo
frame.element=combo
indicator.element=carrow
[ToolTip]
inherits=GenericFrame
frame.top=3
frame.bottom=3
frame.left=3
frame.right=3
interior=true
text.shadow=0
text.margin=0
interior.element=tooltip
frame.element=tooltip
frame.expansion=0

View File

@ -1,255 +0,0 @@
/* GTK 3.0 Theme - Generated by pywal */
/* This theme uses colors extracted from wallpapers */
/* Define color variables */
@define-color theme_bg_color {background};
@define-color theme_fg_color {foreground};
@define-color theme_base_color {background};
@define-color theme_text_color {foreground};
@define-color theme_selected_bg_color {color4};
@define-color theme_selected_fg_color {background};
@define-color insensitive_bg_color shade({background}, 0.95);
@define-color insensitive_fg_color shade({foreground}, 0.7);
@define-color insensitive_base_color {background};
@define-color theme_unfocused_bg_color {background};
@define-color theme_unfocused_fg_color {foreground};
@define-color theme_unfocused_base_color {background};
@define-color theme_unfocused_text_color {foreground};
@define-color theme_unfocused_selected_bg_color {color4};
@define-color theme_unfocused_selected_fg_color {background};
@define-color borders shade({background}, 0.8);
@define-color unfocused_borders shade({background}, 0.85);
/* Additional accent colors */
@define-color accent_bg_color {color4};
@define-color accent_fg_color {background};
@define-color accent_color {color4};
@define-color destructive_bg_color {color1};
@define-color destructive_fg_color {background};
@define-color destructive_color {color1};
@define-color success_bg_color {color2};
@define-color success_fg_color {background};
@define-color success_color {color2};
@define-color warning_bg_color {color3};
@define-color warning_fg_color {background};
@define-color warning_color {color3};
@define-color error_bg_color {color1};
@define-color error_fg_color {background};
@define-color error_color {color1};
/* Window styling */
window {
background-color: {background};
color: {foreground};
}
/* Widget styling */
* {{
background-color: {background};
color: {foreground};
}}
button {
background-color: {color0};
color: {foreground};
border: 1px solid {color8};
border-radius: 4px;
padding: 6px 12px;
}
button:hover {
background-color: {color8};
border-color: {color4};
}
button:active,
button:checked {
background-color: {color4};
color: {background};
}
button:disabled {
background-color: shade({background}, 0.95);
color: shade({foreground}, 0.7);
}
/* Entry/Input fields */
entry {
background-color: {color0};
color: {foreground};
border: 1px solid {color8};
border-radius: 4px;
padding: 6px;
}
entry:focus {
border-color: {color4};
}
entry:disabled {
background-color: shade({background}, 0.95);
color: shade({foreground}, 0.7);
}
/* Selections */
selection,
*:selected {
background-color: {color4};
color: {background};
}
/* Headerbar */
headerbar {
background-color: {color0};
color: {foreground};
border-bottom: 1px solid {color8};
}
headerbar button {
background-color: transparent;
border: none;
}
headerbar button:hover {
background-color: {color8};
}
/* Sidebar */
.sidebar {
background-color: {color0};
border-right: 1px solid {color8};
}
/* Notebook/Tabs */
notebook > header {
background-color: {color0};
border-bottom: 1px solid {color8};
}
notebook > header > tabs > tab {
background-color: transparent;
color: {foreground};
padding: 6px 12px;
}
notebook > header > tabs > tab:checked {
background-color: {background};
border-bottom: 2px solid {color4};
}
/* Menu */
menu,
.menu {
background-color: {color0};
color: {foreground};
border: 1px solid {color8};
}
menuitem {
padding: 6px 12px;
}
menuitem:hover {
background-color: {color4};
color: {background};
}
/* Scrollbar */
scrollbar {
background-color: {background};
}
scrollbar slider {
background-color: {color8};
border-radius: 8px;
min-width: 8px;
min-height: 8px;
}
scrollbar slider:hover {
background-color: {color4};
}
/* Progressbar */
progressbar {
background-color: {color0};
border-radius: 4px;
}
progressbar progress {
background-color: {color4};
border-radius: 4px;
}
/* Switch */
switch {
background-color: {color0};
border: 1px solid {color8};
border-radius: 12px;
}
switch:checked {
background-color: {color4};
}
switch slider {
background-color: {foreground};
border-radius: 50%;
}
/* Checkbutton & Radiobutton */
checkbutton check,
radiobutton radio {
background-color: {color0};
border: 1px solid {color8};
}
checkbutton check:checked,
radiobutton radio:checked {
background-color: {color4};
border-color: {color4};
}
/* Tooltip */
tooltip {
background-color: {color0};
color: {foreground};
border: 1px solid {color8};
border-radius: 4px;
padding: 6px;
}
/* Popover */
popover {
background-color: {color0};
color: {foreground};
border: 1px solid {color8};
border-radius: 8px;
padding: 6px;
}
/* Frame */
frame {
border: 1px solid {color8};
border-radius: 4px;
}
/* List */
list,
.view {
background-color: {background};
color: {foreground};
}
list row {
padding: 6px;
}
list row:hover {
background-color: {color8};
}
list row:selected {
background-color: {color4};
color: {background};
}

View File

@ -1,397 +0,0 @@
/* GTK 4.0 Theme - Generated by pywal */
/* This theme uses colors extracted from wallpapers */
/* Define color variables */
@define-color window_bg_color {background};
@define-color window_fg_color {foreground};
@define-color view_bg_color {background};
@define-color view_fg_color {foreground};
@define-color accent_bg_color {color4};
@define-color accent_fg_color {background};
@define-color accent_color {color4};
@define-color destructive_bg_color {color1};
@define-color destructive_fg_color {background};
@define-color destructive_color {color1};
@define-color success_bg_color {color2};
@define-color success_fg_color {background};
@define-color success_color {color2};
@define-color warning_bg_color {color3};
@define-color warning_fg_color {background};
@define-color warning_color {color3};
@define-color error_bg_color {color1};
@define-color error_fg_color {background};
@define-color error_color {color1};
@define-color headerbar_bg_color {color0};
@define-color headerbar_fg_color {foreground};
@define-color headerbar_border_color {color8};
@define-color sidebar_bg_color {color0};
@define-color sidebar_fg_color {foreground};
@define-color card_bg_color {color0};
@define-color card_fg_color {foreground};
@define-color popover_bg_color {color0};
@define-color popover_fg_color {foreground};
@define-color dialog_bg_color {background};
@define-color dialog_fg_color {foreground};
/* Legacy GTK3 compatibility colors */
@define-color theme_bg_color {background};
@define-color theme_fg_color {foreground};
@define-color theme_base_color {background};
@define-color theme_text_color {foreground};
@define-color theme_selected_bg_color {color4};
@define-color theme_selected_fg_color {background};
@define-color insensitive_bg_color alpha({foreground}, 0.1);
@define-color insensitive_fg_color alpha({foreground}, 0.5);
@define-color borders alpha({foreground}, 0.2);
/* Window styling */
window {
background-color: @window_bg_color;
color: @window_fg_color;
}
/* Widget styling */
* {
background-color: @window_bg_color;
color: @window_fg_color;
}
/* Button */
button {
background-color: @card_bg_color;
color: @window_fg_color;
border: 1px solid @borders;
border-radius: 6px;
padding: 6px 12px;
min-height: 24px;
}
button:hover {
background-color: alpha(@accent_bg_color, 0.1);
border-color: @accent_color;
}
button:active {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
button:disabled {
background-color: @insensitive_bg_color;
color: @insensitive_fg_color;
}
button.suggested-action {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
button.destructive-action {
background-color: @destructive_bg_color;
color: @destructive_fg_color;
}
/* Entry/Input fields */
entry {
background-color: @view_bg_color;
color: @view_fg_color;
border: 1px solid @borders;
border-radius: 6px;
padding: 6px;
min-height: 32px;
}
entry:focus {
border-color: @accent_color;
outline: 2px solid alpha(@accent_color, 0.3);
outline-offset: -1px;
}
entry:disabled {
background-color: @insensitive_bg_color;
color: @insensitive_fg_color;
}
/* Selections */
selection,
*:selected {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
/* Headerbar */
headerbar {
background-color: @headerbar_bg_color;
color: @headerbar_fg_color;
border-bottom: 1px solid @headerbar_border_color;
min-height: 46px;
padding: 0 6px;
}
headerbar button {
background-color: transparent;
border: none;
}
headerbar button:hover {
background-color: alpha(@accent_bg_color, 0.1);
}
headerbar button:active {
background-color: alpha(@accent_bg_color, 0.2);
}
/* Sidebar */
.sidebar {
background-color: @sidebar_bg_color;
border-right: 1px solid @borders;
}
.sidebar row {
padding: 6px;
}
.sidebar row:hover {
background-color: alpha(@accent_bg_color, 0.1);
}
.sidebar row:selected {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
/* Notebook/Tabs */
notebook > header {
background-color: @card_bg_color;
border-bottom: 1px solid @borders;
}
notebook > header > tabs > tab {
background-color: transparent;
color: @window_fg_color;
padding: 6px 12px;
min-height: 32px;
}
notebook > header > tabs > tab:hover {
background-color: alpha(@accent_bg_color, 0.1);
}
notebook > header > tabs > tab:checked {
background-color: @window_bg_color;
border-bottom: 2px solid @accent_color;
}
/* Menu */
menubar {
background-color: @headerbar_bg_color;
color: @headerbar_fg_color;
}
menu,
.menu {
background-color: @popover_bg_color;
color: @popover_fg_color;
border: 1px solid @borders;
border-radius: 8px;
padding: 4px;
}
menuitem {
padding: 6px 12px;
border-radius: 4px;
min-height: 28px;
}
menuitem:hover {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
/* Scrollbar */
scrollbar {
background-color: transparent;
}
scrollbar slider {
background-color: alpha(@window_fg_color, 0.3);
border-radius: 8px;
min-width: 10px;
min-height: 10px;
}
scrollbar slider:hover {
background-color: alpha(@window_fg_color, 0.5);
}
scrollbar slider:active {
background-color: @accent_color;
}
/* Progressbar */
progressbar {
background-color: @view_bg_color;
border-radius: 4px;
}
progressbar progress {
background-color: @accent_bg_color;
border-radius: 4px;
}
progressbar trough {
background-color: alpha(@window_fg_color, 0.1);
border-radius: 4px;
}
/* Switch */
switch {
background-color: alpha(@window_fg_color, 0.2);
border-radius: 12px;
min-width: 48px;
min-height: 24px;
}
switch:checked {
background-color: @accent_bg_color;
}
switch slider {
background-color: @window_fg_color;
border-radius: 50%;
min-width: 20px;
min-height: 20px;
margin: 2px;
}
/* Checkbutton & Radiobutton */
checkbutton check,
radiobutton radio {
background-color: @view_bg_color;
border: 1px solid @borders;
min-width: 16px;
min-height: 16px;
}
checkbutton check {
border-radius: 4px;
}
radiobutton radio {
border-radius: 50%;
}
checkbutton check:checked,
radiobutton radio:checked {
background-color: @accent_bg_color;
border-color: @accent_bg_color;
color: @accent_fg_color;
}
/* Tooltip */
tooltip {
background-color: @popover_bg_color;
color: @popover_fg_color;
border: 1px solid @borders;
border-radius: 6px;
padding: 6px 8px;
}
tooltip label {
padding: 0;
}
/* Popover */
popover {
background-color: @popover_bg_color;
color: @popover_fg_color;
border: 1px solid @borders;
border-radius: 12px;
padding: 6px;
}
popover > contents {
background-color: transparent;
}
/* Frame */
frame {
border: 1px solid @borders;
border-radius: 6px;
}
/* List */
list,
listview,
.view {
background-color: @view_bg_color;
color: @view_fg_color;
border-radius: 6px;
}
list row,
listview row {
padding: 6px;
border-radius: 4px;
}
list row:hover,
listview row:hover {
background-color: alpha(@accent_bg_color, 0.1);
}
list row:selected,
listview row:selected {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
/* Card */
.card {
background-color: @card_bg_color;
color: @card_fg_color;
border: 1px solid @borders;
border-radius: 12px;
padding: 12px;
}
/* Dialog */
dialog {
background-color: @dialog_bg_color;
color: @dialog_fg_color;
}
/* Searchbar */
searchbar {
background-color: @headerbar_bg_color;
border-bottom: 1px solid @borders;
}
/* Toolbar */
toolbar {
background-color: @headerbar_bg_color;
padding: 4px;
border-bottom: 1px solid @borders;
}
/* Infobar */
.info {
background-color: @accent_bg_color;
color: @accent_fg_color;
}
.warning {
background-color: @warning_bg_color;
color: @warning_fg_color;
}
.error {
background-color: @error_bg_color;
color: @error_fg_color;
}
.success,
.question {
background-color: @success_bg_color;
color: @success_fg_color;
}

View File

@ -1,153 +0,0 @@
# Pywal Minimal - Judicious Syntax Highlighting
# Uses only 4 colors for syntax
# Functions and variables remain plain text for reduced visual noise
# Colors dynamically generated from wallpaper via pywal
"ui.background" = {{ bg = "base" }}
"ui.virtual" = {{ fg = "surface0" }}
"ui.virtual.ruler" = {{ bg = "surface0" }}
"ui.virtual.indent-guide" = {{ fg = "surface0" }}
"ui.virtual.inlay-hint" = {{ fg = "overlay1", bg = "mantle", modifiers = ["italic"] }}
"ui.virtual.jump-label" = {{ fg = "red", modifiers = ["bold"] }}
"ui.selection" = {{ fg = "text", bg = "surface1" }}
"ui.selection.primary" = {{ fg = "text", bg = "surface2" }}
"ui.cursor" = {{ fg = "base", bg = "cursor" }}
"ui.cursor.primary" = {{ fg = "base", bg = "cursor" }}
"ui.cursor.match" = {{ fg = "color1", modifiers = ["bold"] }}
"ui.cursorline.primary" = {{ bg = "surface0" }}
"ui.cursorcolumn.primary" = {{ bg = "surface0" }}
"ui.linenr" = {{ fg = "surface1" }}
"ui.linenr.selected" = {{ fg = "cursor", modifiers = ["bold"] }}
"ui.statusline" = {{ fg = "text", bg = "mantle" }}
"ui.statusline.inactive" = {{ fg = "overlay0", bg = "mantle" }}
"ui.statusline.normal" = {{ fg = "base", bg = "cursor", modifiers = ["bold"] }}
"ui.statusline.insert" = {{ fg = "base", bg = "color2", modifiers = ["bold"] }}
"ui.statusline.select" = {{ fg = "base", bg = "color5", modifiers = ["bold"] }}
"ui.bufferline" = {{ fg = "overlay0", bg = "mantle" }}
"ui.bufferline.active" = {{ fg = "cursor", bg = "base", modifiers = ["bold"] }}
"ui.help" = {{ fg = "text", bg = "surface0" }}
"ui.text" = "text"
"ui.text.focus" = {{ fg = "text", bg = "surface0" }}
"ui.text.inactive" = "overlay1"
"ui.menu" = {{ fg = "text", bg = "surface0" }}
"ui.menu.selected" = {{ fg = "text", bg = "surface1", modifiers = ["bold"] }}
"ui.menu.scroll" = {{ fg = "overlay0", bg = "surface0" }}
"ui.popup" = {{ fg = "text", bg = "surface0" }}
"ui.window" = {{ fg = "base" }}
"diagnostic.error" = {{ underline = {{ color = "color1", style = "curl" }} }}
"diagnostic.warning" = {{ underline = {{ color = "color3", style = "curl" }} }}
"diagnostic.info" = {{ underline = {{ color = "color6", style = "curl" }} }}
"diagnostic.hint" = {{ underline = {{ color = "color4", style = "curl" }} }}
"diagnostic.unnecessary" = {{ modifiers = ["dim"] }}
"diagnostic.deprecated" = {{ modifiers = ["crossed_out"] }}
"error" = "color1"
"warning" = "color3"
"info" = "color6"
"hint" = "color4"
"diff.plus" = "color2"
"diff.minus" = "color1"
"diff.delta" = "color3"
"markup.heading" = {{ fg = "cursor", modifiers = ["bold"] }}
"markup.list" = "color5"
"markup.bold" = {{ modifiers = ["bold"] }}
"markup.italic" = {{ modifiers = ["italic"] }}
"markup.strikethrough" = {{ modifiers = ["crossed_out"] }}
"markup.link.url" = {{ fg = "color4", modifiers = ["underlined"] }}
"markup.link.text" = "color5"
"markup.quote" = "color2"
"markup.raw" = "color2"
# Minimal syntax highlighting - only 4 colors used
"comment" = "color3" # Comments pop (yellow/bright color)
"keyword" = {{ fg = "color5", modifiers = ["italic"] }} # Keywords highlighted and italic (magenta)
"keyword.control" = {{ fg = "color5", modifiers = ["italic"] }}
"keyword.directive" = {{ fg = "color5", modifiers = ["italic"] }}
"keyword.function" = {{ fg = "color5", modifiers = ["italic"] }}
"keyword.operator" = {{ fg = "color5", modifiers = ["italic"] }}
"keyword.return" = {{ fg = "color5", modifiers = ["italic"] }}
"keyword.storage" = {{ fg = "color5", modifiers = ["italic"] }}
"string" = "color2" # Strings highlighted (green)
"string.regexp" = "color1"
"string.special" = "color2"
"constant.numeric" = "color1" # Numbers highlighted (orange/red)
"constant.builtin" = "color1"
"constant.character.escape" = "color1"
# Everything else remains plain text
"function" = "text" # Functions are plain text
"function.builtin" = "text"
"function.method" = "text"
"function.macro" = "text"
"variable" = "text" # Variables are plain text
"variable.builtin" = "text"
"variable.parameter" = "text"
"variable.other.member" = "text"
"type" = {{ fg = "cursor", modifiers = ["italic"]}} # Types are slightly highlighted
"type.builtin" = "cursor"
"constructor" = "text" # Constructors are plain text
"attribute" = "text"
"label" = "text"
"namespace" = "text"
"tag" = "text"
# Top-level declarations get accent color
"function.definition" = {{ fg = "color7", modifiers = ["bold"] }}
"type.definition" = {{ fg = "color7", modifiers = ["bold"] }}
# Punctuation is slightly dimmed but still readable
"punctuation" = "subtext0"
"punctuation.bracket" = "subtext0"
"punctuation.delimiter" = "subtext0"
"punctuation.special" = "subtext0"
"operator" = "subtext0" # Operators slightly dimmed
[palette]
# Pywal generated colors - background to foreground gradient
base = "{background}"
mantle = "{color0}"
crust = "{color0}"
surface0 = "{color8}"
surface1 = "{color8}"
surface2 = "{color7}"
overlay0 = "{color8}"
overlay1 = "{color7}"
overlay2 = "{color7}"
subtext0 = "{color7}"
subtext1 = "{color15}"
text = "{foreground}"
# Core colors from pywal palette
red = "{color1}"
color1 = "{color1}" # Red/Orange - numbers, errors
color2 = "{color2}" # Green - strings
color3 = "{color3}" # Yellow - comments
color4 = "{color4}" # Blue - info
color5 = "{color5}" # Magenta - keywords
color6 = "{color6}" # Cyan - hints
color7 = "{color7}" # Light gray - definitions
color15 = "{color15}" # Bright white
cursor = "{cursor}" # Cursor color

View File

@ -1,164 +0,0 @@
# Pywal Semantic - Intelligent color mapping based on semantic meaning
# Colors are automatically mapped based on hue analysis:
# - Reds/oranges for errors and warnings
# - High contrast colors for comments
# - Appropriate semantic colors for syntax elements
# Colors dynamically generated from wallpaper via pywal with semantic mapping
"ui.background" = {{ bg = "base" }}
"ui.virtual" = {{ fg = "surface0" }}
"ui.virtual.ruler" = {{ bg = "surface0" }}
"ui.virtual.indent-guide" = {{ fg = "surface0" }}
"ui.virtual.inlay-hint" = {{ fg = "overlay1", bg = "mantle", modifiers = ["italic"] }}
"ui.virtual.jump-label" = {{ fg = "error_color", modifiers = ["bold"] }}
"ui.selection" = {{ fg = "text", bg = "surface1" }}
"ui.selection.primary" = {{ fg = "text", bg = "surface2" }}
"ui.cursor" = {{ fg = "base", bg = "cursor" }}
"ui.cursor.primary" = {{ fg = "base", bg = "cursor" }}
"ui.cursor.match" = {{ fg = "keyword_color", modifiers = ["bold"] }}
"ui.cursorline.primary" = {{ bg = "surface0" }}
"ui.cursorcolumn.primary" = {{ bg = "surface0" }}
"ui.linenr" = {{ fg = "surface1" }}
"ui.linenr.selected" = {{ fg = "cursor", modifiers = ["bold"] }}
"ui.statusline" = {{ fg = "text", bg = "mantle" }}
"ui.statusline.inactive" = {{ fg = "overlay0", bg = "mantle" }}
"ui.statusline.normal" = {{ fg = "base", bg = "cursor", modifiers = ["bold"] }}
"ui.statusline.insert" = {{ fg = "base", bg = "string_color", modifiers = ["bold"] }}
"ui.statusline.select" = {{ fg = "base", bg = "keyword_color", modifiers = ["bold"] }}
"ui.bufferline" = {{ fg = "overlay0", bg = "mantle" }}
"ui.bufferline.active" = {{ fg = "cursor", bg = "base", modifiers = ["bold"] }}
"ui.help" = {{ fg = "text", bg = "surface0" }}
"ui.text" = "text"
"ui.text.focus" = {{ fg = "text", bg = "surface0" }}
"ui.text.inactive" = "overlay1"
"ui.menu" = {{ fg = "text", bg = "surface0" }}
"ui.menu.selected" = {{ fg = "text", bg = "surface1", modifiers = ["bold"] }}
"ui.menu.scroll" = {{ fg = "overlay0", bg = "surface0" }}
"ui.popup" = {{ fg = "text", bg = "surface0" }}
"ui.window" = {{ fg = "base" }}
"diagnostic.error" = {{ underline = {{ color = "error_color", style = "curl" }} }}
"diagnostic.warning" = {{ underline = {{ color = "warning_color", style = "curl" }} }}
"diagnostic.info" = {{ underline = {{ color = "info_color", style = "curl" }} }}
"diagnostic.hint" = {{ underline = {{ color = "info_color", style = "curl" }} }}
"diagnostic.unnecessary" = {{ modifiers = ["dim"] }}
"diagnostic.deprecated" = {{ modifiers = ["crossed_out"] }}
"error" = "error_color"
"warning" = "warning_color"
"info" = "info_color"
"hint" = "info_color"
"diff.plus" = "string_color"
"diff.minus" = "error_color"
"diff.delta" = "warning_color"
"markup.heading" = {{ fg = "cursor", modifiers = ["bold"] }}
"markup.list" = "keyword_color"
"markup.bold" = {{ modifiers = ["bold"] }}
"markup.italic" = {{ modifiers = ["italic"] }}
"markup.strikethrough" = {{ modifiers = ["crossed_out"] }}
"markup.link.url" = {{ fg = "info_color", modifiers = ["underlined"] }}
"markup.link.text" = "keyword_color"
"markup.quote" = "string_color"
"markup.raw" = "string_color"
# Semantic syntax highlighting
"comment" = "comment_color" # Highest contrast color for visibility
"keyword" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.control" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.directive" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.function" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.operator" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.return" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"keyword.storage" = {{ fg = "keyword_color", modifiers = ["italic"] }}
"string" = "string_color"
"string.regexp" = "error_color"
"string.special" = "string_color"
"constant.numeric" = "number_color"
"constant.builtin" = "number_color"
"constant.character.escape" = "number_color"
# Everything else remains plain text
"function" = "text"
"function.builtin" = "text"
"function.method" = "text"
"function.macro" = "text"
"variable" = "text"
"variable.builtin" = "text"
"variable.parameter" = "text"
"variable.other.member" = "text"
"type" = {{ fg = "cursor", modifiers = ["italic"]}}
"type.builtin" = "cursor"
"constructor" = "text"
"attribute" = "text"
"label" = "text"
"namespace" = "text"
"tag" = "text"
# Top-level declarations get accent color
"function.definition" = {{ fg = "color7", modifiers = ["bold"] }}
"type.definition" = {{ fg = "color7", modifiers = ["bold"] }}
# Punctuation is slightly dimmed but still readable
"punctuation" = "subtext0"
"punctuation.bracket" = "subtext0"
"punctuation.delimiter" = "subtext0"
"punctuation.special" = "subtext0"
"operator" = "subtext0"
[palette]
# Pywal generated colors - background to foreground gradient
base = "{background}"
mantle = "{color0}"
crust = "{color0}"
surface0 = "{color8}"
surface1 = "{color8}"
surface2 = "{color7}"
overlay0 = "{color8}"
overlay1 = "{color7}"
overlay2 = "{color7}"
subtext0 = "{color7}"
subtext1 = "{color15}"
text = "{foreground}"
# Core colors from pywal palette
red = "{color1}"
color1 = "{color1}"
color2 = "{color2}"
color3 = "{color3}"
color4 = "{color4}"
color5 = "{color5}"
color6 = "{color6}"
color7 = "{color7}"
color15 = "{color15}"
cursor = "{cursor}"
# Semantic colors (will be replaced by color_mapper.py)
error_color = "{{ERROR_COLOR}}"
warning_color = "{{WARNING_COLOR}}"
comment_color = "{{COMMENT_COLOR}}"
string_color = "{{STRING_COLOR}}"
keyword_color = "{{KEYWORD_COLOR}}"
number_color = "{{NUMBER_COLOR}}"
info_color = "{{INFO_COLOR}}"

View File

@ -1,297 +0,0 @@
#!/usr/bin/env python3
"""
Theme Switcher Script - Time-based light/dark theme switching.
This script:
1. Determines current theme mode (light/dark) based on time
2. Updates wpaperd config to use appropriate wallpaper directory
3. Restarts wpaperd to load new config
4. Gets the selected wallpaper from wpaperd
5. Generates pywal color palette from the wallpaper
6. Applies GTK/Kvantum/Helix themes
"""
import os
import subprocess
import sys
import time
import shutil
from datetime import datetime
from pathlib import Path
from typing import Optional
import logging
import argparse
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
class ThemeSwitcher:
"""Handles time-based light/dark theme switching."""
def __init__(self, wallpaper_path: str, light_time: str, dark_time: str,
backend: str, rotation_interval: str, wpaperd_mode: str,
sorting: str, apply_themes_script: str):
self.wallpaper_path = Path(wallpaper_path)
self.light_hour = int(light_time.split(':')[0])
self.dark_hour = int(dark_time.split(':')[0])
self.backend = backend
self.rotation_interval = rotation_interval
self.wpaperd_mode = wpaperd_mode
self.sorting = sorting
self.apply_themes_script = apply_themes_script
self.home = Path.home()
# Find command paths
self.systemctl = self._find_command('systemctl')
self.wpaperctl = self._find_command('wpaperctl')
self.wal = self._find_command('wal')
def _find_command(self, cmd: str) -> str:
"""Find full path to a command."""
path = shutil.which(cmd)
if path is None:
# Try common NixOS locations
common_paths = [
f'/run/current-system/sw/bin/{cmd}',
f'/etc/profiles/per-user/{os.environ.get("USER", "")}/bin/{cmd}',
f'{self.home}/.nix-profile/bin/{cmd}',
]
for p in common_paths:
if Path(p).exists():
return p
raise FileNotFoundError(f"Command not found: {cmd}")
return path
def is_wpaperd_running(self) -> bool:
"""Check if wpaperd process is running."""
try:
pgrep = shutil.which('pgrep') or 'pgrep'
result = subprocess.run(
[pgrep, '-x', 'wpaperd'],
capture_output=True,
timeout=5
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def determine_mode(self) -> tuple[str, Path]:
"""
Determine current theme mode based on time.
Returns:
Tuple of (mode_name, wallpaper_directory)
"""
current_hour = datetime.now().hour
if self.light_hour <= current_hour < self.dark_hour:
mode = "light"
wallpaper_dir = self.wallpaper_path / "Light"
else:
mode = "dark"
wallpaper_dir = self.wallpaper_path / "Dark"
return mode, wallpaper_dir
def update_wpaperd_config(self, wallpaper_dir: Path) -> None:
"""
Update wpaperd configuration file.
Args:
wallpaper_dir: Path to wallpaper directory (Light or Dark)
"""
config_dir = self.home / ".config/wpaperd"
config_file = config_dir / "config.toml"
# Ensure config directory exists
config_dir.mkdir(parents=True, exist_ok=True)
# Remove old config file
if config_file.exists():
config_file.unlink()
# Create new config
config_content = f"""[default]
path = "{wallpaper_dir}"
duration = "{self.rotation_interval}"
mode = "{self.wpaperd_mode}"
sorting = "{self.sorting}"
"""
config_file.write_text(config_content)
logger.info(f"Updated wpaperd config: {config_file}")
def restart_wpaperd(self) -> None:
"""Restart wpaperd systemd service."""
try:
subprocess.run(
[self.systemctl, '--user', 'restart', 'wpaperd.service'],
check=True,
timeout=10
)
logger.info("Restarted wpaperd service")
# Give wpaperd time to start and load wallpaper
time.sleep(2)
except subprocess.CalledProcessError as e:
logger.error(f"Failed to restart wpaperd: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout restarting wpaperd")
raise
def get_current_wallpaper(self) -> Path:
"""
Get the current wallpaper path from wpaperd.
Returns:
Path to current wallpaper file
Raises:
RuntimeError: If unable to get wallpaper path
"""
wallpaper_state_dir = self.home / ".local/state/wpaperd/wallpapers"
if not wallpaper_state_dir.exists():
raise RuntimeError(f"Wpaperd state directory not found: {wallpaper_state_dir}")
# Get first monitor directory
try:
monitor_dirs = list(wallpaper_state_dir.iterdir())
if not monitor_dirs:
raise RuntimeError("No monitor directories found in wpaperd state")
monitor_name = monitor_dirs[0].name
# Query wpaperd for current wallpaper
result = subprocess.run(
[self.wpaperctl, 'get-wallpaper', monitor_name],
capture_output=True,
text=True,
check=True,
timeout=5
)
wallpaper_path = result.stdout.strip()
if not wallpaper_path:
raise RuntimeError("wpaperctl returned empty wallpaper path")
return Path(wallpaper_path)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, IndexError) as e:
raise RuntimeError(f"Failed to get current wallpaper: {e}")
def generate_palette(self, wallpaper: Path, mode: str) -> None:
"""
Generate pywal color palette from wallpaper.
Args:
wallpaper: Path to wallpaper image
mode: Theme mode ('light' or 'dark')
"""
cmd = [self.wal, '-i', str(wallpaper), '--backend', self.backend, '-n']
if mode == 'light':
cmd.append('-l')
try:
subprocess.run(cmd, check=True, timeout=30)
logger.info(f"Generated {mode} palette from: {wallpaper}")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to generate palette: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while generating palette")
raise
def apply_themes(self) -> None:
"""Apply GTK and Kvantum themes using helper script."""
try:
subprocess.run(
['python3', self.apply_themes_script],
check=True,
timeout=30
)
logger.info("Applied themes successfully")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to apply themes: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while applying themes")
raise
def run(self) -> int:
"""
Execute the theme switching process.
Returns:
Exit code (0 for success, 1 for failure)
"""
try:
# Determine mode and wallpaper directory
mode, wallpaper_dir = self.determine_mode()
logger.info(f"Switching to {mode} theme with wallpapers from {wallpaper_dir}")
# Check if wpaperd is running
if not self.is_wpaperd_running():
logger.error("wpaperd is not running")
return 1
# Update wpaperd config
self.update_wpaperd_config(wallpaper_dir)
# Restart wpaperd
self.restart_wpaperd()
# Get current wallpaper
current_wallpaper = self.get_current_wallpaper()
logger.info(f"Current wallpaper: {current_wallpaper}")
# Generate palette
self.generate_palette(current_wallpaper, mode)
# Apply themes
self.apply_themes()
logger.info(f"Theme switched to {mode} mode successfully!")
return 0
except Exception as e:
logger.error(f"Theme switching failed: {e}", exc_info=True)
return 1
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description='Time-based theme switcher')
parser.add_argument('--wallpaper-path', required=True, help='Path to wallpaper directories')
parser.add_argument('--light-time', default='09:00:00', help='Light theme start time (HH:MM:SS)')
parser.add_argument('--dark-time', default='16:30:00', help='Dark theme start time (HH:MM:SS)')
parser.add_argument('--backend', default='haishoku', help='Pywal color extraction backend')
parser.add_argument('--rotation-interval', default='5m', help='Wallpaper rotation interval')
parser.add_argument('--wpaperd-mode', default='center', help='Wpaperd display mode')
parser.add_argument('--sorting', default='random', help='Wallpaper sorting method')
parser.add_argument('--apply-themes-script', required=True, help='Path to apply-themes script')
args = parser.parse_args()
switcher = ThemeSwitcher(
wallpaper_path=args.wallpaper_path,
light_time=args.light_time,
dark_time=args.dark_time,
backend=args.backend,
rotation_interval=args.rotation_interval,
wpaperd_mode=args.wpaperd_mode,
sorting=args.sorting,
apply_themes_script=args.apply_themes_script
)
return switcher.run()
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,306 +0,0 @@
#!/usr/bin/env python3
"""
Wallpaper Rotation Script - Handles continuous wallpaper rotation with palette regeneration.
This script:
1. Checks if wpaperd is running
2. Determines current theme mode (light/dark) for correct pywal flags
3. Pauses wpaperd's auto-rotation
4. Advances to next wallpaper
5. Gets the new wallpaper path
6. Regenerates pywal palette
7. Applies themes
8. Resumes wpaperd's auto-rotation
"""
import os
import subprocess
import sys
import time
import shutil
from datetime import datetime
from pathlib import Path
import logging
import argparse
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)
class WallpaperRotator:
"""Handles wallpaper rotation with theme regeneration."""
def __init__(self, light_time: str, dark_time: str, backend: str,
apply_themes_script: str):
self.light_hour = int(light_time.split(':')[0])
self.dark_hour = int(dark_time.split(':')[0])
self.backend = backend
self.apply_themes_script = apply_themes_script
self.home = Path.home()
# Find command paths
self.wpaperctl = self._find_command('wpaperctl')
self.wal = self._find_command('wal')
def _find_command(self, cmd: str) -> str:
"""Find full path to a command."""
path = shutil.which(cmd)
if path is None:
# Try common NixOS locations
common_paths = [
f'/run/current-system/sw/bin/{cmd}',
f'/etc/profiles/per-user/{os.environ.get("USER", "")}/bin/{cmd}',
f'{self.home}/.nix-profile/bin/{cmd}',
]
for p in common_paths:
if Path(p).exists():
return p
raise FileNotFoundError(f"Command not found: {cmd}")
return path
def is_wpaperd_running(self) -> bool:
"""Check if wpaperd process is running."""
try:
pgrep = shutil.which('pgrep') or 'pgrep'
result = subprocess.run(
[pgrep, '-x', 'wpaperd'],
capture_output=True,
timeout=5
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def determine_mode(self) -> tuple[str, list[str]]:
"""
Determine current theme mode and pywal flags based on time.
Returns:
Tuple of (mode_name, pywal_flags_list)
"""
current_hour = datetime.now().hour
if self.light_hour <= current_hour < self.dark_hour:
mode = "light"
pywal_flags = ['-l', '-n']
else:
mode = "dark"
pywal_flags = ['-n']
return mode, pywal_flags
def pause_wpaperd(self) -> None:
"""Pause wpaperd's automatic wallpaper rotation."""
try:
subprocess.run(
[self.wpaperctl, 'pause-wallpaper'],
check=True,
timeout=5,
capture_output=True
)
logger.debug("Paused wpaperd rotation")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to pause wpaperd: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while pausing wpaperd")
raise
except FileNotFoundError:
logger.error("wpaperctl command not found")
raise
def resume_wpaperd(self) -> None:
"""Resume wpaperd's automatic wallpaper rotation."""
try:
subprocess.run(
[self.wpaperctl, 'resume-wallpaper'],
check=True,
timeout=5,
capture_output=True
)
logger.debug("Resumed wpaperd rotation")
except subprocess.CalledProcessError as e:
logger.warning(f"Failed to resume wpaperd: {e}")
except subprocess.TimeoutExpired:
logger.warning("Timeout while resuming wpaperd")
except FileNotFoundError:
logger.warning("wpaperctl command not found")
def advance_wallpaper(self) -> None:
"""Advance to next wallpaper using wpaperctl."""
try:
subprocess.run(
[self.wpaperctl, 'next-wallpaper'],
check=True,
timeout=5,
capture_output=True
)
logger.info("Advanced to next wallpaper")
# Give wpaperd time to complete transition
time.sleep(1)
except subprocess.CalledProcessError as e:
logger.error(f"Failed to advance wallpaper: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while advancing wallpaper")
raise
except FileNotFoundError:
logger.error("wpaperctl command not found")
raise
def get_current_wallpaper(self) -> Path:
"""
Get the current wallpaper path from wpaperd.
Returns:
Path to current wallpaper file
Raises:
RuntimeError: If unable to get wallpaper path
"""
wallpaper_state_dir = self.home / ".local/state/wpaperd/wallpapers"
if not wallpaper_state_dir.exists():
raise RuntimeError(f"Wpaperd state directory not found: {wallpaper_state_dir}")
# Get first monitor directory
try:
monitor_dirs = list(wallpaper_state_dir.iterdir())
if not monitor_dirs:
raise RuntimeError("No monitor directories found in wpaperd state")
monitor_name = monitor_dirs[0].name
# Query wpaperd for current wallpaper
result = subprocess.run(
[self.wpaperctl, 'get-wallpaper', monitor_name],
capture_output=True,
text=True,
check=True,
timeout=5
)
wallpaper_path = result.stdout.strip()
if not wallpaper_path:
raise RuntimeError("wpaperctl returned empty wallpaper path")
return Path(wallpaper_path)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, IndexError) as e:
raise RuntimeError(f"Failed to get current wallpaper: {e}")
def generate_palette(self, wallpaper: Path, pywal_flags: list[str]) -> None:
"""
Generate pywal color palette from wallpaper.
Args:
wallpaper: Path to wallpaper image
pywal_flags: List of additional pywal flags
"""
cmd = [self.wal, '-i', str(wallpaper), '--backend', self.backend] + pywal_flags
try:
subprocess.run(cmd, check=True, timeout=30, capture_output=True)
logger.info(f"Generated palette from: {wallpaper.name}")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to generate palette: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while generating palette")
raise
except FileNotFoundError:
logger.error("wal command not found")
raise
def apply_themes(self) -> None:
"""Apply GTK and Kvantum themes using helper script."""
try:
subprocess.run(
['python3', self.apply_themes_script],
check=True,
timeout=30,
capture_output=True
)
logger.debug("Applied themes successfully")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to apply themes: {e}")
raise
except subprocess.TimeoutExpired:
logger.error("Timeout while applying themes")
raise
except FileNotFoundError as e:
logger.error(f"Theme application script not found: {e}")
raise
def run(self) -> int:
"""
Execute the wallpaper rotation and theme regeneration process.
Returns:
Exit code (0 for success, 1 for failure)
"""
# Check if wpaperd is running
if not self.is_wpaperd_running():
logger.info("wpaperd is not running, skipping rotation")
return 0
try:
# Determine mode
mode, pywal_flags = self.determine_mode()
logger.info(f"Rotating wallpaper and regenerating {mode} palette...")
# Pause wpaperd
self.pause_wpaperd()
try:
# Advance to next wallpaper
self.advance_wallpaper()
# Get new wallpaper path
current_wallpaper = self.get_current_wallpaper()
logger.info(f"New wallpaper: {current_wallpaper.name}")
# Generate palette
self.generate_palette(current_wallpaper, pywal_flags)
# Apply themes
self.apply_themes()
logger.info("Wallpaper rotated and palette regenerated successfully!")
return 0
finally:
# Always resume wpaperd, even if something fails
self.resume_wpaperd()
except Exception as e:
logger.error(f"Wallpaper rotation failed: {e}", exc_info=True)
return 1
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description='Wallpaper rotation with theme regeneration')
parser.add_argument('--light-time', default='09:00:00', help='Light theme start time (HH:MM:SS)')
parser.add_argument('--dark-time', default='16:30:00', help='Dark theme start time (HH:MM:SS)')
parser.add_argument('--backend', default='haishoku', help='Pywal color extraction backend')
parser.add_argument('--apply-themes-script', required=True, help='Path to apply-themes script')
args = parser.parse_args()
rotator = WallpaperRotator(
light_time=args.light_time,
dark_time=args.dark_time,
backend=args.backend,
apply_themes_script=args.apply_themes_script
)
return rotator.run()
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,100 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.wallpaperRotator;
in
{
options.services.wallpaperRotator = {
enable = lib.mkEnableOption "wallpaper rotation service using wpaperd";
user = lib.mkOption {
type = lib.types.str;
description = "Username for the wallpaper service";
};
wallpaperPath = lib.mkOption {
type = lib.types.str;
default = "/home/${cfg.user}/nixos/shared/modules/services/wallpapers";
description = "Path to the wallpaper directory";
};
duration = lib.mkOption {
type = lib.types.str;
default = "5m";
example = "30s";
description = "How long to display each wallpaper (e.g., 30s, 5m, 1h)";
};
sorting = lib.mkOption {
type = lib.types.enum [ "random" "ascending" "descending" ];
default = "random";
description = "Order in which to display wallpapers";
};
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 = "hexagonalize";
description = ''
Transition effect name. Available effects include:
fade, hexagonalize, simple-zoom, swirl, circle, wipe, slide, etc.
See wpaperd documentation for full list.
'';
};
duration = lib.mkOption {
type = lib.types.int;
default = 300;
example = 2000;
description = "Transition duration in milliseconds";
};
};
queueSize = lib.mkOption {
type = lib.types.int;
default = 10;
description = "Size of wallpaper history queue for random mode";
};
initialTransition = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable transition effect at wpaperd startup";
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${cfg.user} = {
services.wpaperd = {
enable = true;
settings = {
# Default configuration for all displays
default = {
path = cfg.wallpaperPath;
duration = cfg.duration;
sorting = cfg.sorting;
mode = cfg.mode;
transition-time = cfg.transition.duration;
queue-size = cfg.queueSize;
initial-transition = cfg.initialTransition;
};
# Apply transition effect
"default.transition.${cfg.transition.effect}" = {};
};
};
# Add wpaperd CLI tool to user packages for manual control
home.packages = with pkgs; [
wpaperd
];
};
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

View File

@ -1,26 +0,0 @@
# Wallpapers
This directory contains purchased wallpapers that are licensed for personal use only.
## License
These images are purchased and copyrighted by their respective creators.
They are **not included in this git repository** and are licensed for personal use only.
No reproduction, distribution, or commercial use is permitted.
## Usage
Place your wallpaper images in this directory. The wallpaper rotation service
will automatically detect and rotate through them.
Supported formats:
- JPEG (.jpg, .jpeg)
- PNG (.png)
- WebP (.webp)
- AVIF (.avif)
## Configuration
The wallpaper rotation service is configured via the `services.wallpaperRotator`
option in your host configuration. See the module documentation for available options.