Files
Modus/modules/launcher/plugins/wallpaper.py
T

597 lines
24 KiB
Python

"""
Wallpaper plugin for the launcher.
Provides wallpaper management with search, random selection, matugen integration, and hex color support.
"""
import os
import random
import hashlib
import colorsys
import re
from typing import List, Dict
from PIL import Image
from gi.repository import GdkPixbuf
from modules.launcher.plugin_base import PluginBase
from modules.launcher.result import Result
from fabric.utils.helpers import exec_shell_command_async
import config.data as data
import utils.icons as icons
class WallpaperPlugin(PluginBase):
"""
Plugin for wallpaper management with search, random selection, and matugen integration.
"""
def __init__(self):
super().__init__()
self.wallpapers = []
self.cache_dir = f"{data.CACHE_DIR}/thumbs"
self.schemes = {
"scheme-tonal-spot": "Tonal Spot",
"scheme-content": "Content",
"scheme-expressive": "Expressive",
"scheme-fidelity": "Fidelity",
"scheme-fruit-salad": "Fruit Salad",
"scheme-monochrome": "Monochrome",
"scheme-neutral": "Neutral",
"scheme-rainbow": "Rainbow",
}
def initialize(self):
"""Initialize the wallpaper plugin."""
self.set_triggers(["wall", "wall "])
self._load_wallpapers()
os.makedirs(self.cache_dir, exist_ok=True)
def cleanup(self):
"""Cleanup the wallpaper plugin."""
pass
def _load_wallpapers(self):
"""Load available wallpapers from the wallpapers directory."""
try:
if os.path.exists(data.WALLPAPERS_DIR):
self.wallpapers = sorted(
[f for f in os.listdir(data.WALLPAPERS_DIR) if self._is_image(f)]
)
else:
print(f"Wallpapers directory not found: {data.WALLPAPERS_DIR}")
except Exception as e:
print(f"Error loading wallpapers: {e}")
def _is_image(self, filename: str) -> bool:
"""Check if file is a supported image format."""
return filename.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
def _get_matugen_state(self) -> bool:
"""Get current matugen state from file."""
try:
if os.path.exists(data.MATUGEN_STATE_FILE):
with open(data.MATUGEN_STATE_FILE, "r") as f:
content = f.read().strip().lower()
return content == "true"
return True # Default to True
except Exception as e:
print(f"Error reading matugen state: {e}")
return True
def _set_matugen_state(self, enabled: bool):
"""Set matugen state to file."""
try:
os.makedirs(os.path.dirname(data.MATUGEN_STATE_FILE), exist_ok=True)
with open(data.MATUGEN_STATE_FILE, "w") as f:
f.write("true" if enabled else "false")
# Show notification for immediate feedback
status = "enabled" if enabled else "disabled"
exec_shell_command_async(
f"notify-send '🎨 Matugen' 'Dynamic colors {status}' -a '{
data.APP_NAME_CAP
}' -e"
)
except Exception as e:
print(f"Error setting matugen state: {e}")
def _get_cache_path(self, filename: str) -> str:
"""Get cache path for wallpaper thumbnail."""
file_hash = hashlib.md5(filename.encode("utf-8")).hexdigest()
return os.path.join(self.cache_dir, f"{file_hash}.png")
def _create_thumbnail(self, filename: str) -> str:
"""Create thumbnail for wallpaper if it doesn't exist."""
full_path = os.path.join(data.WALLPAPERS_DIR, filename)
cache_path = self._get_cache_path(filename)
if not os.path.exists(cache_path):
try:
with Image.open(full_path) as img:
# Use faster thumbnail creation
img.thumbnail((48, 48), Image.Resampling.LANCZOS)
img.save(cache_path, "PNG")
except Exception as e:
print(f"Error creating thumbnail for {filename}: {e}")
return None
return cache_path
def _set_wallpaper(self, filename: str, scheme: str = None):
"""Set wallpaper with optional matugen integration."""
full_path = os.path.join(data.WALLPAPERS_DIR, filename)
current_wall = os.path.expanduser("~/.current.wall")
if scheme is None:
scheme = self._get_current_scheme()
# Update current wallpaper symlink
if os.path.isfile(current_wall) or os.path.islink(current_wall):
os.remove(current_wall)
os.symlink(full_path, current_wall)
# Apply wallpaper with or without matugen
matugen_enabled = self._get_matugen_state()
if matugen_enabled:
exec_shell_command_async(f'matugen image "{full_path}" -t {scheme}')
else:
exec_shell_command_async(
f'swww img "{
full_path
}" -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest'
)
def _set_random_wallpaper(self):
"""Set a random wallpaper."""
if not self.wallpapers:
return
filename = random.choice(self.wallpapers)
self._set_wallpaper(filename)
# Show notification for immediate feedback
exec_shell_command_async(
f"notify-send '🎲 Random Wallpaper' 'Applied: {filename}' -a '{
data.APP_NAME_CAP
}' -e"
)
return filename
def _hsl_to_rgb_hex(self, h: float, s: float = 1.0, l: float = 0.5) -> str:
"""Convert HSL color value to RGB HEX string."""
# colorsys uses HLS, not HSL, and expects values between 0.0 and 1.0
hue = h / 360.0
r, g, b = colorsys.hls_to_rgb(hue, l, s) # Note the order: H, L, S
r_int, g_int, b_int = int(r * 255), int(g * 255), int(b * 255)
return f"#{r_int:02X}{g_int:02X}{b_int:02X}"
def _is_valid_hex_color(self, hex_color: str) -> bool:
"""Check if string is a valid hex color."""
if not hex_color.startswith("#"):
hex_color = "#" + hex_color
return bool(re.match(r"^#[0-9A-Fa-f]{6}$", hex_color))
def _get_current_scheme(self) -> str:
"""Get current color scheme (default to tonal-spot)."""
# You could store this in a config file, for now use default
return "scheme-tonal-spot"
def _set_current_scheme(self, scheme: str):
"""Set current color scheme."""
# For now, just use it immediately. Could be stored in config later.
scheme_name = self.schemes.get(scheme, scheme)
matugen_enabled = self._get_matugen_state()
# Show notification with matugen status
if matugen_enabled:
exec_shell_command_async(
f"notify-send '🎨 Color Scheme' 'Set to {
scheme_name
}\\nMatugen: Enabled' -a '{data.APP_NAME_CAP}' -e"
)
else:
exec_shell_command_async(
f"notify-send '⚠️ Color Scheme' 'Set to {
scheme_name
}\\nMatugen: Disabled (enable for effect)' -a '{data.APP_NAME_CAP}' -e"
)
def _apply_hex_color(self, hex_color: str, scheme: str = None):
"""Apply hex color using matugen."""
if not hex_color.startswith("#"):
hex_color = "#" + hex_color
if scheme is None:
scheme = self._get_current_scheme()
exec_shell_command_async(f'matugen color hex "{hex_color}" -t {scheme}')
scheme_name = self.schemes.get(scheme, scheme)
exec_shell_command_async(
f"notify-send '🎨 Hex Color Applied' 'Color: {hex_color}\\nScheme: {
scheme_name
}' -a '{data.APP_NAME_CAP}' -e"
)
def _generate_random_hex_color(self) -> str:
"""Generate a random hex color."""
hue = random.randint(0, 360)
return self._hsl_to_rgb_hex(hue)
def _get_status_indicators(self) -> tuple:
"""Get current status indicators for display."""
current_scheme = self._get_current_scheme()
matugen_enabled = self._get_matugen_state()
scheme_name = self.schemes.get(current_scheme, current_scheme)
indicators = []
if not matugen_enabled:
indicators.append("⚠ Matugen Off")
indicator_text = " • " + " • ".join(indicators) if indicators else ""
status_text = f"Matugen: {
'Enabled' if matugen_enabled else 'Disabled'
} • Scheme: {scheme_name}"
return indicator_text, status_text, current_scheme, matugen_enabled
def query(self, query_string: str) -> List[Result]:
"""Search wallpapers and provide management options."""
results = []
query = query_string.lower().strip()
# Get status indicators for consistent display
indicator_text, status_text, current_scheme, matugen_enabled = (
self._get_status_indicators()
)
# Special commands
if query == "random" or query.startswith("random"):
# Execute immediately when this result is created/selected
self._set_random_wallpaper()
results.append(
Result(
title=f"Random Wallpaper Applied{indicator_text}",
subtitle=f"A random wallpaper has been set • {status_text}",
icon_markup=random.choice(
[
icons.dice_1,
icons.dice_2,
icons.dice_3,
icons.dice_4,
icons.dice_5,
icons.dice_6,
]
),
action=lambda: None, # No action needed since already executed
relevance=1.0,
plugin_name=self.display_name,
data={
"action": "random",
"executed": True,
"bypass_max_results": True,
},
)
)
# Hex color commands
if "color" in query or "hex" in query or query.startswith("#"):
# Check for scheme specification in the query
scheme = self._get_current_scheme()
for scheme_id, scheme_name in self.schemes.items():
if (
scheme_name.lower() in query.lower()
or scheme_id.lower() in query.lower()
):
scheme = scheme_id
break
# Check for hex color patterns
hex_match = re.search(r"#?([0-9A-Fa-f]{6})", query)
if hex_match:
hex_color = "#" + hex_match.group(1)
# Execute immediately
self._apply_hex_color(hex_color, scheme)
scheme_name = self.schemes.get(scheme, scheme)
results.append(
Result(
title=f"Hex Color Applied: {hex_color}{indicator_text}",
subtitle=f"Applied with {scheme_name} scheme • {status_text}",
icon_markup=icons.palette,
action=lambda: None, # No action needed since already executed
relevance=1.0,
plugin_name=self.display_name,
data={
"action": "hex_color",
"color": hex_color,
"scheme": scheme,
"executed": True,
"bypass_max_results": True,
},
)
)
elif "random" in query:
# Random hex color
hex_color = self._generate_random_hex_color()
self._apply_hex_color(hex_color, scheme)
scheme_name = self.schemes.get(scheme, scheme)
results.append(
Result(
title=f"Random Color Applied: {hex_color}{indicator_text}",
subtitle=f"Applied with {scheme_name} scheme • {status_text}",
icon_markup=icons.palette,
action=lambda: None, # No action needed since already executed
relevance=1.0,
plugin_name=self.display_name,
data={
"action": "random_hex",
"color": hex_color,
"scheme": scheme,
"executed": True,
"bypass_max_results": True,
},
)
)
else:
# Show hex color help
results.append(
Result(
title="Hex Color Commands",
subtitle="Use: color #FF5733, hex #00FF00, color random, or add scheme name",
icon_markup=icons.palette,
action=lambda: None,
relevance=0.8,
plugin_name=self.display_name,
data={"action": "hex_help", "bypass_max_results": True},
)
)
# Color scheme commands
if "scheme" in query:
current_scheme = self._get_current_scheme()
matugen_enabled = self._get_matugen_state()
# Show all available schemes
for scheme_id, scheme_name in self.schemes.items():
# Check if this scheme matches the query (for filtering)
if (
query.strip() == "scheme"
or scheme_name.lower() in query.lower()
or scheme_id.lower() in query.lower()
):
# Create indicators
indicators = []
if scheme_id == current_scheme:
indicators.append("● Current")
if not matugen_enabled:
indicators.append("⚠ Matugen Off")
indicator_text = (
" • " + " • ".join(indicators) if indicators else ""
)
results.append(
Result(
title=f"{scheme_name}{indicator_text}",
subtitle=f"Set color scheme to {scheme_name}"
+ (
" (requires matugen enabled)"
if not matugen_enabled
else ""
),
icon_markup=icons.palette,
action=lambda s=scheme_id: self._set_current_scheme(s),
relevance=1.0 if scheme_id == current_scheme else 0.8,
plugin_name=self.display_name,
data={
"action": "scheme_select",
"scheme": scheme_id,
"bypass_max_results": True,
},
)
)
# Matugen controls
if "matugen" in query:
current_state = self._get_matugen_state()
if "on" in query or "enable" in query:
# Execute immediately
self._set_matugen_state(True)
# Get updated status after change
new_indicator_text, new_status_text, _, _ = (
self._get_status_indicators()
)
results.append(
Result(
title=f"Matugen Enabled{new_indicator_text}",
subtitle=f"Dynamic color generation is now enabled • {
new_status_text
}",
icon_markup=icons.palette,
action=lambda: None, # No action needed since already executed
relevance=0.9,
plugin_name=self.display_name,
data={
"action": "matugen_on",
"executed": True,
"bypass_max_results": True,
},
)
)
elif "off" in query or "disable" in query:
# Execute immediately
self._set_matugen_state(False)
# Get updated status after change
new_indicator_text, new_status_text, _, _ = (
self._get_status_indicators()
)
results.append(
Result(
title=f"Matugen Disabled{new_indicator_text}",
subtitle=f"Dynamic color generation is now disabled • {
new_status_text
}",
icon_markup=icons.palette,
action=lambda: None, # No action needed since already executed
relevance=0.9,
plugin_name=self.display_name,
data={
"action": "matugen_off",
"executed": True,
"bypass_max_results": True,
},
)
)
elif "toggle" in query:
# Execute toggle immediately
new_state = not current_state
self._set_matugen_state(new_state)
# Get updated status after change
new_indicator_text, new_status_text, _, _ = (
self._get_status_indicators()
)
results.append(
Result(
title=f"Matugen {'Enabled' if new_state else 'Disabled'}{
new_indicator_text
}",
subtitle=f"Dynamic color generation is now {
'enabled' if new_state else 'disabled'
} • {new_status_text}",
icon_markup=icons.palette,
action=lambda: None, # No action needed since already executed
relevance=0.9,
plugin_name=self.display_name,
data={
"action": "matugen_toggle",
"executed": True,
"bypass_max_results": True,
},
)
)
else:
# Show current state with scheme info
results.append(
Result(
title=f"Matugen: {'Enabled' if current_state else 'Disabled'}{
indicator_text
}",
subtitle=f"Dynamic colors • {status_text}",
icon_markup=icons.palette,
action=lambda: None,
relevance=0.8,
plugin_name=self.display_name,
data={"action": "matugen_status", "bypass_max_results": True},
)
)
# Status command
if query == "status" or query == "info":
results.append(
Result(
title=f"Wallpaper System Status{indicator_text}",
subtitle=status_text,
icon_markup=icons.palette,
action=lambda: None,
relevance=1.0,
plugin_name=self.display_name,
data={"action": "status", "bypass_max_results": True},
)
)
# Handle empty query (just trigger keywords: wall, wallpaper, wp)
if not query:
# Show status and quick actions when just typing trigger keywords
results.append(
Result(
title=f"Wallpaper System{indicator_text}",
subtitle=f"{
status_text
} • Type commands: random, scheme, status, matugen",
icon_markup=icons.wallpapers,
action=lambda: None,
relevance=1.0,
plugin_name=self.display_name,
data={"action": "overview", "bypass_max_results": True},
)
)
# Show quick actions
results.append(
Result(
title=f"Random Wallpaper{indicator_text}",
subtitle=f"Set a random wallpaper • {status_text}",
icon_markup=random.choice(
[
icons.dice_1,
icons.dice_2,
icons.dice_3,
icons.dice_4,
icons.dice_5,
icons.dice_6,
]
),
action=lambda: self._set_random_wallpaper(),
relevance=0.9,
plugin_name=self.display_name,
data={"action": "random_quick", "bypass_max_results": True},
)
)
# Search wallpapers by filename
if not query or (
query
and "matugen" not in query
and "random" not in query
and "scheme" not in query
and "status" not in query
and "info" not in query
and "color" not in query
and "hex" not in query
):
matching_wallpapers = []
for wallpaper in self.wallpapers:
if not query or query in wallpaper.lower():
# Calculate relevance
relevance = 1.0 if query == wallpaper.lower() else 0.7
if query and query in wallpaper.lower():
relevance = 0.8
matching_wallpapers.append((wallpaper, relevance))
# Sort by relevance - no limit since we use bypass_max_results
matching_wallpapers.sort(key=lambda x: x[1], reverse=True)
for wallpaper, relevance in matching_wallpapers:
# Only create thumbnails for displayed results
cache_path = self._get_cache_path(wallpaper)
icon = None
# Try to load existing thumbnail first
if os.path.exists(cache_path):
try:
icon = GdkPixbuf.Pixbuf.new_from_file_at_scale(
cache_path, 32, 32, True
)
except Exception as e:
print(f"Error loading existing thumbnail: {e}")
results.append(
Result(
title=f"{wallpaper}{indicator_text if not query else ''}",
subtitle=f"Set as wallpaper{
' • ' + status_text if not query else ''
}",
icon=icon,
icon_markup=icons.wallpapers if not icon else None,
action=lambda w=wallpaper: self._set_wallpaper(w),
relevance=relevance,
plugin_name=self.display_name,
data={
"wallpaper": wallpaper,
"action": "set",
"bypass_max_results": True,
},
)
)
# Sort by relevance - no limit since we use bypass_max_results
results.sort(key=lambda x: x.relevance, reverse=True)
return results