298 lines
10 KiB
Python
Executable File
298 lines
10 KiB
Python
Executable File
#!/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())
|