add kiki plymouth boot, remove old wallpaper switcher stuff, add background images
5
.ignore
@ -1,4 +1,7 @@
|
|||||||
# dotfiles/
|
# dotfiles/
|
||||||
.git/
|
.git/
|
||||||
flake.lock
|
flake.lock
|
||||||
frame12/framework-plymouth-theme
|
frame12/framework-plymouth-theme/framework/throbber-*
|
||||||
|
|
||||||
|
*.png
|
||||||
|
*.jpeg
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
// gui startup
|
// gui startup
|
||||||
|
spawn-at-startup "swaybg" "-i" "/home/nate/nixos/frame12/wallpaper.png" "-m" "fill"
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
spawn-at-startup "keepassxc"
|
spawn-at-startup "keepassxc"
|
||||||
spawn-at-startup "flatpak" "run" "org.signal.Signal"
|
spawn-at-startup "flatpak" "run" "org.signal.Signal"
|
||||||
|
|||||||
@ -65,6 +65,19 @@ in
|
|||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
boot = {
|
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"
|
# Enable "Silent Boot"
|
||||||
consoleLogLevel = 0;
|
consoleLogLevel = 0;
|
||||||
initrd.verbose = false;
|
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
|
// gui startup
|
||||||
|
spawn-at-startup "swaybg" "-i" "/home/jaci/nixos/jaci/kiki_background.jpg" "-m" "fill"
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
spawn-at-startup "keepassxc"
|
spawn-at-startup "keepassxc"
|
||||||
// shell startup
|
// shell startup
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
# Stylix theming - auto-generate color scheme from wallpaper
|
# Stylix theming - auto-generate color scheme from wallpaper
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
image = ../shared/modules/services/wallpapers/Dark/IU-Dark.jpg;
|
image = ./wallpaper.png;
|
||||||
polarity = "dark";
|
polarity = "dark";
|
||||||
|
|
||||||
# System-wide cursor
|
# System-wide cursor
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
// gui startup
|
// gui startup
|
||||||
|
spawn-at-startup "swaybg" "-i" "/home/nate/nixos/nate-work/wallpaper.png" "-m" "fill"
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
spawn-at-startup "keepassxc"
|
spawn-at-startup "keepassxc"
|
||||||
spawn-at-startup "flatpak" "run" "org.signal.Signal"
|
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 theming - auto-generate color scheme from wallpaper
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
image = ../shared/modules/services/wallpapers/Dark/FSO-Dark.jpg;
|
image = ./wallpaper.png;
|
||||||
polarity = "dark";
|
polarity = "dark";
|
||||||
|
|
||||||
# System-wide cursor
|
# 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.
|
|
||||||