add kiki plymouth boot, remove old wallpaper switcher stuff, add background images
5
.ignore
@ -1,4 +1,7 @@
|
||||
# dotfiles/
|
||||
.git/
|
||||
flake.lock
|
||||
frame12/framework-plymouth-theme
|
||||
frame12/framework-plymouth-theme/framework/throbber-*
|
||||
|
||||
*.png
|
||||
*.jpeg
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
jaci/kiki-plymouth-theme/kiki.gif
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/bullet.png
Normal file
|
After Width: | Height: | Size: 616 B |
BIN
jaci/kiki-plymouth-theme/kiki/capslock.png
Normal file
|
After Width: | Height: | Size: 960 B |
BIN
jaci/kiki-plymouth-theme/kiki/entry.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/keyboard.png
Normal file
|
After Width: | Height: | Size: 946 B |
BIN
jaci/kiki-plymouth-theme/kiki/keymap-render.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
54
jaci/kiki-plymouth-theme/kiki/kiki.plymouth
Normal 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
|
||||
BIN
jaci/kiki-plymouth-theme/kiki/lock.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0001.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0002.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0003.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0004.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0005.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0006.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0007.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0008.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0009.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0010.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0011.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0012.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0013.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0014.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0015.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0016.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0017.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0018.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0019.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0020.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0021.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0022.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0023.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
jaci/kiki-plymouth-theme/kiki/throbber-0024.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
After Width: | Height: | Size: 2.7 MiB |
@ -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
|
After Width: | Height: | Size: 68 KiB |
@ -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)
|
||||
@ -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/`
|
||||
@ -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
|
||||
@ -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
|
||||
```
|
||||
@ -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())
|
||||
@ -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())
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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}}"
|
||||
@ -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())
|
||||
@ -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())
|
||||
@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 3.7 MiB |
@ -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.
|
||||