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