From 814ee1b9fdcbc403fc15e5a1cd7c8880fe3989f9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 22 Jun 2025 19:38:03 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85=20style(colors):=20update=20color?= =?UTF-8?q?=20scheme=20for=20better=20contrast=20and=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/hypr/colors.conf | 22 +- config/hypr/modus.conf | 14 +- main.py | 2 +- modules/launcher/plugins/wallpaper.py | 959 +++++++++++++++++++++++++ modules/launcher/plugins/wallpapers.py | 543 -------------- styles/colors.css | 28 +- 6 files changed, 992 insertions(+), 576 deletions(-) create mode 100644 modules/launcher/plugins/wallpaper.py delete mode 100644 modules/launcher/plugins/wallpapers.py diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf index fd9e674..d7ffce5 100644 --- a/config/hypr/colors.conf +++ b/config/hypr/colors.conf @@ -1,13 +1,13 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/173.png -$background = 121318 -$foreground = e3e1e9 +$wallpaper = /home/sankalp/Pictures/wallpapers/122.jpg +$background = 111318 +$foreground = e2e2e9 -$primary = bac3ff -$secondary = c3c5dd -$tertiary = e5bad8 -$surface = 121318 -$surface_bright = 38393f -$outline = 90909a +$primary = adc6ff +$secondary = bfc6dc +$tertiary = debcdf +$surface = 111318 +$surface_bright = 37393e +$outline = 8e9099 $error = ff5945 $shadow = 000000 @@ -15,13 +15,13 @@ $shadow = 000000 $red = ffb2b9 $green = 95d5a7 $yellow = b8cf84 -$blue = bcc2ff +$blue = b6c4ff $magenta = e4b7f3 $cyan = 82d3e2 $white = 82d3e0 $red_dim = ff4c5c $green_dim = 4cb86a $yellow_dim = 8baa43 -$blue_dim = 5665ff +$blue_dim = 5072ff $magenta_dim = c360e4 $cyan_dim = 30b5ce diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index cbccf5d..8d9cebf 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -10,24 +10,24 @@ $axMessage = notify-send "Modus" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i " # Reload Modus bind = ALT SHIFT, R, exec, killall modus; uwsm-app $(python /home/sankalp/Modus/main.py) # Message -bind = SUPER, A, exec, $axMessage +bind = SUPER, A, exec, $Message # Application Switcher bind = ALT, TAB, exec, $fabricSend 'switcher.show_switcher()' # Kanban -bind = SUPER, T, exec, $fabricSend "launcher.show_launcher('kanban ')" # Kanban +bind = SUPER, T, exec, $fabricSend "launcher.show_launcher('kanban')" # Kanban # App Launcher bind = SUPER, SPACE, exec, $fabricSend "launcher.show_launcher()" -bind = SUPER, D, exec, $fabricSend "launcher.show_launcher('app ')" +bind = SUPER, D, exec, $fabricSend "launcher.show_launcher('app')" # Clipboard History -bind = SUPER, V, exec, $fabricSend "launcher.show_launcher('clip ')" +bind = SUPER, V, exec, $fabricSend "launcher.show_launcher('clip')" # Wallpapers -bind = SUPER, W, exec, $fabricSend "launcher.show_launcher('wall ')" +bind = SUPER, W, exec, $fabricSend "launcher.show_launcher('wall')" # Random Wallpaper bind = SHIFT ALT, W, exec, $fabricSend "launcher.show_launcher('wall random', external=True)" # Emoji Picker -bind = SUPER, E, exec, $fabricSend "launcher.show_launcher('emoji ')" +bind = SUPER, E, exec, $fabricSend "launcher.show_launcher('emoji')" # Power Menu -bind = SUPER, ESCAPE, exec, $fabricSend "launcher.show_launcher('power ')" +bind = SUPER, ESCAPE, exec, $fabricSend "launcher.show_launcher('power')" # Toggle Caffeine bind = SUPER SHIFT, M, exec, $fabricSend "launcher.show_launcher('caffeine on', external=True)" # Settings diff --git a/main.py b/main.py index 1d3cdba..52dfa6a 100644 --- a/main.py +++ b/main.py @@ -35,7 +35,7 @@ if __name__ == "__main__": current_wallpaper = os.path.expanduser("~/.current.wall") if not os.path.exists(current_wallpaper): example_wallpaper = os.path.expanduser( - f"~/{APP_NAME_CAP}/assets/wallpapers_example/example-1.jpg" + f"~/{APP_NAME_CAP}/assets/wallpapers_example/example-1.png" ) os.symlink(example_wallpaper, current_wallpaper) diff --git a/modules/launcher/plugins/wallpaper.py b/modules/launcher/plugins/wallpaper.py new file mode 100644 index 0000000..7b31436 --- /dev/null +++ b/modules/launcher/plugins/wallpaper.py @@ -0,0 +1,959 @@ +import colorsys +import hashlib +import json +import os +import random +import re +import threading +import time +from typing import Dict, List, Optional + +import config.data as data +import utils.icons as icons +from fabric.utils.helpers import exec_shell_command_async +from gi.repository import GdkPixbuf +from modules.launcher.plugin_base import PluginBase +from modules.launcher.result import Result +from PIL import Image + + +class WallpaperPlugin(PluginBase): + """ + Plugin for wallpaper management with search, random selection, and matugen integration. + """ + + def __init__(self): + super().__init__() + self.display_name = "Wallpaper" + self.wallpapers = [] + self.cache_dir = f"{data.CACHE_DIR}/thumbs" + self.thumbnail_cache: Dict[str, Optional[GdkPixbuf.Pixbuf]] = {} + self.thumbnail_loading = set() # Track which thumbnails are being loaded + 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"]) + self._load_wallpapers() + os.makedirs(self.cache_dir, exist_ok=True) + # Start background thumbnail creation + self._start_background_thumbnail_creation() + + 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 config.json.""" + self.matugen_enabled = True # Default to True + try: + with open(data.CONFIG_FILE, "r") as f: + config = json.load(f) + self.matugen_enabled = config.get("matugen_enabled", True) + except FileNotFoundError: + # File doesn't exist, keep default True + pass + except Exception as e: + print(f"Error reading config file: {e}") + # Keep default True on error + + return self.matugen_enabled + + def _set_matugen_state(self, enabled: bool): + """Set matugen state and save to config.json.""" + self.matugen_enabled = enabled + + # Save the state to config.json + try: + # Read current config + config = {} + if os.path.exists(data.CONFIG_FILE): + with open(data.CONFIG_FILE, "r") as f: + config = json.load(f) + + # Update matugen state + config["matugen_enabled"] = enabled + + # Write back to config file + with open(data.CONFIG_FILE, "w") as f: + json.dump(config, f, indent=4) + + except Exception as e: + print(f"Error writing matugen state to config: {e}") + + # # Send notification + # status = "enabled" if enabled else "disabled" + # exec_shell_command_async( + # f"notify-send '🎨 Matugen' 'Dynamic colors {status}' -a '{ + # data.APP_NAME_CAP + # }' -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 with smaller size for better performance + img.thumbnail((32, 32), Image.Resampling.LANCZOS) + img.save(cache_path, "PNG", optimize=True) + except Exception as e: + print(f"Error creating thumbnail for {filename}: {e}") + return None + + return cache_path + + def _start_background_thumbnail_creation(self): + """Start background thread to create thumbnails for all wallpapers.""" + + def create_thumbnails(): + for wallpaper in self.wallpapers: + if wallpaper not in self.thumbnail_loading: + self.thumbnail_loading.add(wallpaper) + try: + cache_path = self._create_thumbnail(wallpaper) + if cache_path and os.path.exists(cache_path): + # Load thumbnail into memory cache + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + cache_path, 32, 32, True + ) + self.thumbnail_cache[wallpaper] = pixbuf + except Exception as e: + print(f"Error loading thumbnail for {wallpaper}: {e}") + self.thumbnail_cache[wallpaper] = None + else: + self.thumbnail_cache[wallpaper] = None + except Exception as e: + print(f"Error processing thumbnail for {wallpaper}: {e}") + self.thumbnail_cache[wallpaper] = None + finally: + self.thumbnail_loading.discard(wallpaper) + + # Small delay to prevent overwhelming the system + time.sleep(0.01) + + # Start background thread + thread = threading.Thread(target=create_thumbnails, daemon=True) + thread.start() + + def _get_thumbnail_fast(self, filename: str) -> Optional[GdkPixbuf.Pixbuf]: + """Get thumbnail quickly from cache or return None if not ready.""" + # Return cached thumbnail if available + if filename in self.thumbnail_cache: + return self.thumbnail_cache[filename] + + # Check if thumbnail file exists and load it immediately + cache_path = self._get_cache_path(filename) + if os.path.exists(cache_path): + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + cache_path, 32, 32, True + ) + self.thumbnail_cache[filename] = pixbuf + return pixbuf + except Exception as e: + print(f"Error loading thumbnail for {filename}: {e}") + self.thumbnail_cache[filename] = None + return None + + # If not in cache and file doesn't exist, trigger background creation + if filename not in self.thumbnail_loading: + self.thumbnail_loading.add(filename) + + def create_async(): + try: + cache_path = self._create_thumbnail(filename) + if cache_path and os.path.exists(cache_path): + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( + cache_path, 32, 32, True + ) + self.thumbnail_cache[filename] = pixbuf + except Exception as e: + print(f"Error loading thumbnail for {filename}: {e}") + self.thumbnail_cache[filename] = None + else: + self.thumbnail_cache[filename] = None + except Exception as e: + print(f"Error creating thumbnail for {filename}: {e}") + self.thumbnail_cache[filename] = None + finally: + self.thumbnail_loading.discard(filename) + + thread = threading.Thread(target=create_async, daemon=True) + thread.start() + + return None + + 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 from config (default to tonal-spot).""" + try: + with open(data.CONFIG_FILE, "r") as f: + config = json.load(f) + return config.get("current_scheme", "scheme-tonal-spot") + except FileNotFoundError: + # File doesn't exist, return default + return "scheme-tonal-spot" + except Exception as e: + print(f"Error reading current scheme from config: {e}") + return "scheme-tonal-spot" + + def _set_current_scheme(self, scheme: str): + """Set current color scheme and apply it.""" + scheme_name = self.schemes.get(scheme, scheme) + matugen_enabled = self._get_matugen_state() + + # Save the scheme to config + try: + # Read current config + config = {} + if os.path.exists(data.CONFIG_FILE): + with open(data.CONFIG_FILE, "r") as f: + config = json.load(f) + + # Update current scheme + config["current_scheme"] = scheme + + # Write back to config file + with open(data.CONFIG_FILE, "w") as f: + json.dump(config, f, indent=4) + + except Exception as e: + print(f"Error saving current scheme to config: {e}") + return + + # Apply the scheme to current wallpaper if matugen is enabled + if matugen_enabled: + current_wall = os.path.expanduser("~/.current.wall") + if os.path.exists(current_wall) and os.path.islink(current_wall): + # Get the current wallpaper path + wallpaper_path = os.readlink(current_wall) + if os.path.exists(wallpaper_path): + # Apply the new scheme to current wallpaper + exec_shell_command_async(f'matugen image "{wallpaper_path}" -t {scheme}') + + # Send notification + exec_shell_command_async( + f"notify-send '🎨 Color Scheme' 'Applied {scheme_name} scheme' -a '{ + data.APP_NAME_CAP + }' -e" + ) + else: + # No current wallpaper, just show scheme change notification + exec_shell_command_async( + f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (will apply to next wallpaper)' -a '{ + data.APP_NAME_CAP + }' -e" + ) + else: + # No current wallpaper, just show scheme change notification + exec_shell_command_async( + f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (will apply to next wallpaper)' -a '{ + data.APP_NAME_CAP + }' -e" + ) + else: + # Matugen is disabled, just save the setting + exec_shell_command_async( + f"notify-send '🎨 Color Scheme' 'Set to {scheme_name} (matugen disabled)' -a '{ + data.APP_NAME_CAP + }' -e" + ) + + def _apply_hex_color(self, hex_color: str, scheme: str = None): + """Apply hex color using matugen. Assumes matugen is enabled.""" + 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}') + + def _apply_hex_color_direct(self, hex_color: str, scheme: str = None): + """Apply hex color using matugen (following example_wallpapers.py pattern).""" + 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}') + + # Send notification + exec_shell_command_async( + f"notify-send '🎨 Hex Color Applied' 'Applied color: {hex_color}' -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.strip() == "random": + # Show result for random wallpaper (execute on Enter) + 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=1.0, + plugin_name=self.display_name, + data={ + "action": "random", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif query.startswith("random") and query.strip() != "random": + # Show suggestion for random wallpaper (partial match) + 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_suggestion", + "bypass_max_results": True, + "keep_launcher_open": 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) + scheme_name = self.schemes.get(scheme, scheme) + + # Check if this is a complete hex color input (6 digits) + # Execute immediately when we have a complete 6-digit hex color + if len(hex_match.group(1)) == 6: + # Check if there's additional text after the hex color + hex_end_pos = hex_match.end() + remaining_text = query[hex_end_pos:].strip() + + # Only show result if no additional text after hex color (exact match) + if not remaining_text: + # Hex colors work when matugen is OFF (following example_wallpapers.py pattern) + if not matugen_enabled: + # Show result for hex color (execute on Enter) + results.append( + Result( + title=f"Apply Hex Color: {hex_color}{ + indicator_text + }", + subtitle=f"Apply with {scheme_name} scheme • { + status_text + }", + icon_markup=icons.palette, + action=lambda c=hex_color, s=scheme: self._apply_hex_color_direct(c, s), + relevance=1.0, + plugin_name=self.display_name, + data={ + "action": "hex_color", + "color": hex_color, + "scheme": scheme, + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + else: + # Show error result when matugen is enabled + results.append( + Result( + title=f"Cannot Apply Hex Color: {hex_color}{ + indicator_text + }", + subtitle="Matugen is enabled • Disable matugen to use hex colors", + icon_markup=icons.palette, + action=lambda: None, + relevance=1.0, + plugin_name=self.display_name, + data={ + "action": "hex_color_failed", + "color": hex_color, + "bypass_max_results": True, + }, + ) + ) + else: + # Show suggestion for hex color with additional text (partial match) + results.append( + Result( + title=f"Apply Hex Color: {hex_color}{indicator_text}", + subtitle=f"Apply with {scheme_name} scheme • { + status_text + }", + icon_markup=icons.palette, + action=lambda c=hex_color, s=scheme: self._apply_hex_color_direct(c, s) + if not matugen_enabled + else None, + relevance=0.9, + plugin_name=self.display_name, + data={ + "action": "hex_color_suggestion", + "color": hex_color, + "scheme": scheme, + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + else: + # Incomplete hex color, show as suggestion + results.append( + Result( + title=f"Hex Color (incomplete): {hex_color}", + subtitle="Complete the 6-digit hex color to apply", + icon_markup=icons.palette, + action=lambda: None, + relevance=0.7, + plugin_name=self.display_name, + data={ + "action": "hex_color_incomplete", + "color": hex_color, + "bypass_max_results": True, + }, + ) + ) + elif "random" in query and ("color" in query or "hex" in query): + # Check for exact matches for random color commands + if ( + query.strip() == "color random" + or query.strip() == "hex random" + or query.strip() == "random color" + or query.strip() == "random hex" + ): + # Random hex color - show result (execute on Enter) + scheme_name = self.schemes.get(scheme, scheme) + + # Check if matugen is disabled (hex colors work when matugen is OFF) + if not matugen_enabled: + results.append( + Result( + title=f"Random Hex Color{indicator_text}", + subtitle=f"Generate and apply random color with {scheme_name} scheme • {status_text}", + icon_markup=icons.palette, + action=lambda s=scheme: self._apply_hex_color_direct( + self._generate_random_hex_color(), s + ), + relevance=1.0, + plugin_name=self.display_name, + data={ + "action": "random_hex", + "scheme": scheme, + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + else: + # Show error result when matugen is enabled + results.append( + Result( + title=f"Cannot Apply Random Color{indicator_text}", + subtitle="Matugen is enabled • Disable matugen to use hex colors", + icon_markup=icons.palette, + action=lambda: None, + relevance=1.0, + plugin_name=self.display_name, + data={ + "action": "random_hex_failed", + "bypass_max_results": True, + }, + ) + ) + else: + # Show suggestion for random hex color (partial match) + results.append( + Result( + title=f"Random Hex Color{indicator_text}", + subtitle=f"Generate and apply random color • {status_text}", + icon_markup=icons.palette, + action=lambda s=scheme: self._apply_hex_color_direct( + self._generate_random_hex_color(), s + ) + if not matugen_enabled + else None, + relevance=0.8, + plugin_name=self.display_name, + data={ + "action": "random_hex_suggestion", + "scheme": scheme, + "bypass_max_results": True, + "keep_launcher_open": 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": + # Show all schemes when just "scheme" is typed + scheme_matches = True + else: + # Extract search terms from query (remove "scheme" keyword) + search_terms = query.replace("scheme", "").strip().split() + scheme_matches = False + + # Check if any search term matches the scheme name or ID + for term in search_terms: + if ( + term.lower() in scheme_name.lower() + or term.lower() in scheme_id.lower() + ): + scheme_matches = True + break + + if scheme_matches: + # 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, + "keep_launcher_open": True, + }, + ) + ) + + # Matugen controls + if "matugen" in query: + current_state = self._get_matugen_state() + + # Check for exact command matches + if query.strip() == "matugen on" or query.strip() == "matugen enable": + # Show result for enabling matugen (execute on Enter) + results.append( + Result( + title=f"Enable Matugen{indicator_text}", + subtitle=f"Turn on dynamic color generation • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(True), + relevance=0.9, + plugin_name=self.display_name, + data={ + "action": "matugen_on", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif query.strip() == "matugen off" or query.strip() == "matugen disable": + # Show result for disabling matugen (execute on Enter) + results.append( + Result( + title=f"Disable Matugen{indicator_text}", + subtitle=f"Turn off dynamic color generation • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(False), + relevance=0.9, + plugin_name=self.display_name, + data={ + "action": "matugen_off", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif query.strip() == "matugen toggle": + # Show result for toggling matugen (execute on Enter) + new_state = not current_state + results.append( + Result( + title=f"Toggle Matugen to { + 'Enabled' if new_state else 'Disabled' + }{indicator_text}", + subtitle=f"Switch matugen to { + 'enabled' if new_state else 'disabled' + } • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(new_state), + relevance=0.9, + plugin_name=self.display_name, + data={ + "action": "matugen_toggle", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif ("on" in query or "enable" in query) and not query.strip().endswith( + ("on", "enable") + ): + # Show suggestion for enabling (partial match) + results.append( + Result( + title=f"Enable Matugen{indicator_text}", + subtitle=f"Turn on dynamic color generation • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(True), + relevance=0.8, + plugin_name=self.display_name, + data={ + "action": "matugen_on_suggestion", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif ("off" in query or "disable" in query) and not query.strip().endswith( + ("off", "disable") + ): + # Show suggestion for disabling (partial match) + results.append( + Result( + title=f"Disable Matugen{indicator_text}", + subtitle=f"Turn off dynamic color generation • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(False), + relevance=0.8, + plugin_name=self.display_name, + data={ + "action": "matugen_off_suggestion", + "bypass_max_results": True, + "keep_launcher_open": True, + }, + ) + ) + elif "toggle" in query and not query.strip().endswith("toggle"): + # Show suggestion for toggling (partial match) + results.append( + Result( + title=f"Toggle Matugen{indicator_text}", + subtitle=f"Switch matugen state • {status_text}", + icon_markup=icons.palette, + action=lambda: self._set_matugen_state(not current_state), + relevance=0.8, + plugin_name=self.display_name, + data={ + "action": "matugen_toggle_suggestion", + "bypass_max_results": True, + "keep_launcher_open": 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, + "keep_launcher_open": 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 and limit results for better performance + matching_wallpapers.sort(key=lambda x: x[1], reverse=True) + + # Limit to first 50 results for performance (can be increased if needed) + max_results = 50 if query else 20 # Show fewer when no query to load faster + matching_wallpapers = matching_wallpapers[:max_results] + + for wallpaper, relevance in matching_wallpapers: + # Use fast thumbnail loading + icon = self._get_thumbnail_fast(wallpaper) + + 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, + "keep_launcher_open": True, + }, + ) + ) + + # Sort by relevance - no limit since we use bypass_max_results + results.sort(key=lambda x: x.relevance, reverse=True) + return results diff --git a/modules/launcher/plugins/wallpapers.py b/modules/launcher/plugins/wallpapers.py deleted file mode 100644 index 1a24398..0000000 --- a/modules/launcher/plugins/wallpapers.py +++ /dev/null @@ -1,543 +0,0 @@ -import colorsys -import concurrent.futures -import hashlib -import os -import random # <--- AÑADIDO -import shutil -from concurrent.futures import ThreadPoolExecutor - -from fabric.utils.helpers import exec_shell_command_async -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.label import Label -from fabric.widgets.scrolledwindow import ScrolledWindow -from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Pango -from PIL import Image - -import config.config -import config.data as data -import modules.icons as icons - - -class WallpaperSelector(Box): - CACHE_DIR = f"{data.CACHE_DIR}/thumbs" # Changed from wallpapers to thumbs - - def __init__(self, **kwargs): - # Delete the old cache directory if it exists - old_cache_dir = f"{data.CACHE_DIR}/wallpapers" - if os.path.exists(old_cache_dir): - shutil.rmtree(old_cache_dir) - - super().__init__(name="wallpapers", spacing=4, orientation="v", h_expand=False, v_expand=False, **kwargs) - os.makedirs(self.CACHE_DIR, exist_ok=True) - - # Process old wallpapers: use os.scandir for efficiency and only loop - # over image files that actually need renaming (they're not already lowercase - # and with hyphens instead of spaces) - with os.scandir(data.WALLPAPERS_DIR) as entries: - for entry in entries: - if entry.is_file() and self._is_image(entry.name): - # Check if the file needs renaming: file should be lowercase and have hyphens instead of spaces - if entry.name != entry.name.lower() or " " in entry.name: - new_name = entry.name.lower().replace(" ", "-") - full_path = os.path.join(data.WALLPAPERS_DIR, entry.name) - new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name) - try: - os.rename(full_path, new_full_path) - print(f"Renamed old wallpaper '{full_path}' to '{new_full_path}'") - except Exception as e: - print(f"Error renaming file {full_path}: {e}") - - # Refresh the file list after potential renaming - self.files = sorted([f for f in os.listdir(data.WALLPAPERS_DIR) if self._is_image(f)]) - self.thumbnails = [] - self.thumbnail_queue = [] - self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor - - # Variable to control the selection (similar to AppLauncher) - self.selected_index = -1 - - # Initialize UI components - self.viewport = Gtk.IconView(name="wallpaper-icons") - self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str)) - self.viewport.set_pixbuf_column(0) - # Hide text column so only the image is shown - self.viewport.set_text_column(-1) - self.viewport.set_item_width(0) - self.viewport.connect("item-activated", self.on_wallpaper_selected) - # self.viewport.connect("selection-changed", self._on_selection_changed) # Removed connection - - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - h_expand=True, - v_expand=True, - h_align="fill", - v_align="fill", - child=self.viewport, - propagate_width=False, - propagate_height=False, - ) - - self.search_entry = Entry( - name="search-entry-walls", - placeholder="Search Wallpapers...", - h_expand=True, - h_align="fill", - notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()), - on_key_press_event=self.on_search_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - self.search_entry.connect("focus-out-event", self.on_search_entry_focus_out) - - 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", - } - - self.scheme_dropdown = Gtk.ComboBoxText() - self.scheme_dropdown.set_name("scheme-dropdown") - self.scheme_dropdown.set_tooltip_text("Select color scheme") - for key, display_name in self.schemes.items(): - self.scheme_dropdown.append(key, display_name) - self.scheme_dropdown.set_active_id("scheme-tonal-spot") - self.scheme_dropdown.connect("changed", self.on_scheme_changed) - - # Load matugen state from the dedicated file - self.matugen_enabled = True # Default to True - try: - with open(data.MATUGEN_STATE_FILE, 'r') as f: - content = f.read().strip().lower() - if content == "false": - self.matugen_enabled = False - elif content == "true": - self.matugen_enabled = True - # Any other content defaults to True - except FileNotFoundError: - # File doesn't exist, keep default True and create it on first toggle - pass - except Exception as e: - print(f"Error reading matugen state file: {e}") - # Keep default True on error - - # Create a switcher to enable/disable Matugen (enabled by default) - self.matugen_switcher = Gtk.Switch(name="matugen-switcher") - self.matugen_switcher.set_tooltip_text("Toggle dynamic colors") - self.matugen_switcher.set_vexpand(False) - self.matugen_switcher.set_hexpand(False) - self.matugen_switcher.set_valign(Gtk.Align.CENTER) - self.matugen_switcher.set_halign(Gtk.Align.CENTER) - self.matugen_switcher.set_active(self.matugen_enabled) - self.matugen_switcher.connect("notify::active", self.on_switch_toggled) - - self.mat_icon = Label(name="mat-label", markup=icons.palette) - - self.random_wall = Button( - name="random-wall-button", - child=Label(name="random-wall-label", markup=icons.dice_1), - tooltip_text="Random Wallpaper", - ) - self.random_wall.connect("clicked", self.set_random_wallpaper) # <--- AÑADIDO - - # Add the switcher to the header_box's start_children - self.header_box = Box( - name="header-box", - spacing=8, - orientation="h", - children=[self.random_wall, self.search_entry, self.scheme_dropdown, self.matugen_switcher], - ) - - self.add(self.header_box) - - # Create the custom color selector components - self.hue_slider = Gtk.Scale( - orientation=Gtk.Orientation.HORIZONTAL, # Changed from VERTICAL - adjustment=Gtk.Adjustment(value=0, lower=0, upper=360, step_increment=1, page_increment=10), - draw_value=False, # Hide the default value text - digits=0, - # inverted=True, # Removed inverted for horizontal - name="hue-slider", # For CSS styling - ) - - # Changed expand/align for horizontal orientation - self.hue_slider.set_hexpand(True) - self.hue_slider.set_halign(Gtk.Align.FILL) - self.hue_slider.set_vexpand(False) # Ensure it doesn't expand vertically - self.hue_slider.set_valign(Gtk.Align.CENTER) # Center vertically within its box - - self.apply_color_button = Button(name="apply-color-button", child=Label(name="apply-color-label", markup=icons.accept)) - self.apply_color_button.connect("clicked", self.on_apply_color_clicked) - self.apply_color_button.set_vexpand(False) # Ensure button doesn't expand vertically - self.apply_color_button.set_valign(Gtk.Align.CENTER) # Center button vertically - - self.custom_color_selector_box = Box( - orientation="h", spacing=5, name="custom-color-selector-box", # Changed orientation to horizontal - h_align="center" # Center the horizontal box - ) - self.custom_color_selector_box.add(self.hue_slider) - self.custom_color_selector_box.add(self.apply_color_button) - self.custom_color_selector_box.set_halign(Gtk.Align.FILL) - - # Add the scrolled window (grid) and the custom color selector box directly - # to the main WallpaperSelector box (which is already vertical) - self.pack_start(self.scrolled_window, True, True, 0) # Add grid, expand - self.pack_start(self.custom_color_selector_box, False, False, 0) # Add custom selector, don't expand - - # Removed the old main_content_box and its add - - self._start_thumbnail_thread() - self.connect("map", self.on_map) - self.setup_file_monitor() - self.show_all() - self.randomize_dice_icon() - # Ensure the search entry gets focus when starting - self.search_entry.grab_focus() - - def randomize_dice_icon(self): - dice_icons = [ - icons.dice_1, - icons.dice_2, - icons.dice_3, - icons.dice_4, - icons.dice_5, - icons.dice_6, - ] - chosen_icon = random.choice(dice_icons) - label = self.random_wall.get_child() - if isinstance(label, Label): - label.set_markup(chosen_icon) - - def set_random_wallpaper(self, widget, external=False): - if not self.files: - print("No wallpapers available to set a random one.") - return - - file_name = random.choice(self.files) - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - selected_scheme = self.scheme_dropdown.get_active_id() - current_wall = os.path.expanduser(f"~/.current.wall") - - if os.path.isfile(current_wall) or os.path.islink(current_wall): # Check for link too - os.remove(current_wall) - os.symlink(full_path, current_wall) - - if self.matugen_switcher.get_active(): - exec_shell_command_async(f'matugen image "{full_path}" -t {selected_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' - ) - - print(f"Set random wallpaper: {file_name}") - - if external: - exec_shell_command_async(f"notify-send '🎲 Wallpaper' 'Setting a random wallpaper 🎨' -a '{data.APP_NAME_CAP}' -i '{full_path}' -e") - - self.randomize_dice_icon() - - def setup_file_monitor(self): - gfile = Gio.File.new_for_path(data.WALLPAPERS_DIR) - self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None) - self.file_monitor.connect("changed", self.on_directory_changed) - - def on_directory_changed(self, monitor, file, other_file, event_type): - file_name = file.get_basename() - if event_type == Gio.FileMonitorEvent.DELETED: - if file_name in self.files: - self.files.remove(file_name) - cache_path = self._get_cache_path(file_name) - if os.path.exists(cache_path): - try: - os.remove(cache_path) - except Exception as e: - print(f"Error deleting cache {cache_path}: {e}") - self.thumbnails = [(p, n) for p, n in self.thumbnails if n != file_name] - GLib.idle_add(self.arrange_viewport, self.search_entry.get_text()) - elif event_type == Gio.FileMonitorEvent.CREATED: - if self._is_image(file_name): - # Convert filename to lowercase and replace spaces with "-" - new_name = file_name.lower().replace(" ", "-") - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name) - if new_name != file_name: - try: - os.rename(full_path, new_full_path) - file_name = new_name - print(f"Renamed file '{full_path}' to '{new_full_path}')") - except Exception as e: - print(f"Error renaming file {full_path}: {e}") - if file_name not in self.files: - self.files.append(file_name) - self.files.sort() - self.executor.submit(self._process_file, file_name) - elif event_type == Gio.FileMonitorEvent.CHANGED: - if self._is_image(file_name) and file_name in self.files: - cache_path = self._get_cache_path(file_name) - if os.path.exists(cache_path): - try: - os.remove(cache_path) - except Exception as e: - print(f"Error deleting cache for changed file {file_name}: {e}") - self.executor.submit(self._process_file, file_name) - - def arrange_viewport(self, query: str = ""): - model = self.viewport.get_model() - model.clear() - filtered_thumbnails = [ - (thumb, name) - for thumb, name in self.thumbnails - if query.casefold() in name.casefold() - ] - filtered_thumbnails.sort(key=lambda x: x[1].lower()) - for pixbuf, file_name in filtered_thumbnails: - model.append([pixbuf, file_name]) - # If the search entry is empty, no icon is selected; otherwise, select the first one. - if query.strip() == "": - self.viewport.unselect_all() - self.selected_index = -1 - elif len(model) > 0: - self.update_selection(0) - - def on_wallpaper_selected(self, iconview, path): - model = iconview.get_model() - file_name = model[path][1] - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - selected_scheme = self.scheme_dropdown.get_active_id() - current_wall = os.path.expanduser(f"~/.current.wall") - if os.path.isfile(current_wall) or os.path.islink(current_wall): - os.remove(current_wall) - os.symlink(full_path, current_wall) - if self.matugen_switcher.get_active(): - # Matugen is enabled: run the normal command. - exec_shell_command_async(f'matugen image "{full_path}" -t {selected_scheme}') - else: - # Matugen is disabled: run the alternative swww command. - exec_shell_command_async( - f'swww img "{full_path}" -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest' - ) - - def on_scheme_changed(self, combo): - selected_scheme = combo.get_active_id() - print(f"Color scheme selected: {selected_scheme}") - - def on_search_entry_key_press(self, widget, event): - if event.state & Gdk.ModifierType.SHIFT_MASK: - if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down): - schemes_list = list(self.schemes.keys()) - current_id = self.scheme_dropdown.get_active_id() - current_index = schemes_list.index(current_id) if current_id in schemes_list else 0 - new_index = (current_index - 1) % len(schemes_list) if event.keyval == Gdk.KEY_Up else (current_index + 1) % len(schemes_list) - self.scheme_dropdown.set_active(new_index) - return True - elif event.keyval == Gdk.KEY_Right: - self.scheme_dropdown.popup() - return True - - if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right): - self.move_selection_2d(event.keyval) - return True - elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - if self.selected_index != -1: - path = Gtk.TreePath.new_from_indices([self.selected_index]) - self.on_wallpaper_selected(self.viewport, path) - return True - return False - - # Removed _on_selection_changed method - - def move_selection_2d(self, keyval): - model = self.viewport.get_model() - total_items = len(model) - if total_items == 0: - return - - # --- Determine Column Count --- - columns = self.viewport.get_columns() - - # If get_columns returns 0 or -1 (auto), try to estimate by checking item rows - if columns <= 0 and total_items > 0: - estimated_cols = 0 - try: - # Check the row of the first item (should be 0) - first_item_path = Gtk.TreePath.new_from_indices([0]) - base_row = self.viewport.get_item_row(first_item_path) - - # Find the index of the first item in the *next* row - for i in range(1, total_items): - path = Gtk.TreePath.new_from_indices([i]) - row = self.viewport.get_item_row(path) - if row > base_row: - estimated_cols = i # The number of items in the first row - break - - # If loop finished without finding a new row, all items are in one row - if estimated_cols == 0: - estimated_cols = total_items - - columns = max(1, estimated_cols) - except Exception: - # Fallback if get_item_row fails (e.g., widget not realized) - columns = 1 - elif columns <= 0 and total_items == 0: - columns = 1 # Should not happen due to early return, but safe - - # Ensure columns is at least 1 after all checks - columns = max(1, columns) - - # --- Navigation Logic --- - current_index = self.selected_index - new_index = current_index - - if current_index == -1: - # If nothing is selected, select the first or last item based on direction - if keyval in (Gdk.KEY_Down, Gdk.KEY_Right): - new_index = 0 - elif keyval in (Gdk.KEY_Up, Gdk.KEY_Left): - new_index = total_items - 1 - if total_items == 0: new_index = -1 # Handle edge case - - else: - # Calculate potential new index based on key press - if keyval == Gdk.KEY_Up: - potential_new_index = current_index - columns - # Only update if the new index is valid (>= 0) - if potential_new_index >= 0: - new_index = potential_new_index - elif keyval == Gdk.KEY_Down: - potential_new_index = current_index + columns - # Only update if the new index is valid (< total_items) - if potential_new_index < total_items: - new_index = potential_new_index - elif keyval == Gdk.KEY_Left: - # Only update if not already in the first column (index % columns != 0) - # and the index is greater than 0 - if current_index > 0 and current_index % columns != 0: - new_index = current_index - 1 - elif keyval == Gdk.KEY_Right: - # Only update if not in the last column ((index + 1) % columns != 0) - # and not the very last item (index < total_items - 1) - if current_index < total_items - 1 and (current_index + 1) % columns != 0: - new_index = current_index + 1 - - # Only update if the index actually changed and is valid - if new_index != self.selected_index and 0 <= new_index < total_items: - self.update_selection(new_index) - elif total_items > 0 and self.selected_index == -1 and 0 <= new_index < total_items: - # Handle selecting the first item when starting from -1 - self.update_selection(new_index) - - def update_selection(self, new_index: int): - self.viewport.unselect_all() - path = Gtk.TreePath.new_from_indices([new_index]) - self.viewport.select_path(path) - self.viewport.scroll_to_path(path, False, 0.5, 0.5) # Ensure the selected icon is visible - self.selected_index = new_index - - def _start_thumbnail_thread(self): - thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None) - - def _preload_thumbnails(self, _data): - futures = [self.executor.submit(self._process_file, file_name) for file_name in self.files] - concurrent.futures.wait(futures) - GLib.idle_add(self._process_batch) - - def _process_file(self, file_name): - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - cache_path = self._get_cache_path(file_name) - if not os.path.exists(cache_path): - try: - with Image.open(full_path) as img: - width, height = img.size - side = min(width, height) - left = (img.width - side) // 2 - top = (height - side) // 2 - right = left + side - bottom = top + side - img_cropped = img.crop((left, top, right, bottom)) - img_cropped.thumbnail((96, 96), Image.Resampling.LANCZOS) - img_cropped.save(cache_path, "PNG") - except Exception as e: - print(f"Error processing {file_name}: {e}") - return - self.thumbnail_queue.append((cache_path, file_name)) - GLib.idle_add(self._process_batch) - - def _process_batch(self): - batch = self.thumbnail_queue[:10] - del self.thumbnail_queue[:10] - for cache_path, file_name in batch: - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path) - self.thumbnails.append((pixbuf, file_name)) - self.viewport.get_model().append([pixbuf, file_name]) - except Exception as e: - print(f"Error loading thumbnail {cache_path}: {e}") - if self.thumbnail_queue: - GLib.idle_add(self._process_batch) - return False - - def _get_cache_path(self, file_name: str) -> str: - file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest() - return os.path.join(self.CACHE_DIR, f"{file_hash}.png") - - @staticmethod - def _is_image(file_name: str) -> bool: - return file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.webp')) - - def on_search_entry_focus_out(self, widget, event): - if self.get_mapped(): - widget.grab_focus() - return False - - def on_map(self, widget): - """Handles the map signal to set initial visibility of the color selector.""" - # Set visibility based on the loaded state when the widget becomes visible - self.custom_color_selector_box.set_visible(not self.matugen_enabled) - - def hsl_to_rgb_hex(self, h: float, s: float = 1.0, l: float = 0.5) -> str: - """Converts 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 rgba_to_hex(self, rgba: Gdk.RGBA) -> str: - """Converts Gdk.RGBA to a HEX color string.""" - r = int(rgba.red * 255) - g = int(rgba.green * 255) - b = int(rgba.blue * 255) - return f"#{r:02X}{g:02X}{b:02X}" - - def on_switch_toggled(self, switch, gparam): - """Handles the toggling of the Matugen switch.""" - is_active = switch.get_active() - self.matugen_enabled = is_active - # self.scheme_dropdown.set_sensitive(is_active) - self.custom_color_selector_box.set_visible(not is_active) # Toggle visibility - - # Save the state to the dedicated file - try: - with open(data.MATUGEN_STATE_FILE, 'w') as f: - f.write(str(is_active)) - except Exception as e: - print(f"Error writing matugen state file: {e}") - - def on_apply_color_clicked(self, button): - """Applies the color selected by the hue slider via matugen.""" - hue_value = self.hue_slider.get_value() # Get value from 0-360 - hex_color = self.hsl_to_rgb_hex(hue_value) # Convert HSL(hue, 1.0, 0.5) to HEX - print(f"Applying color from slider: H={hue_value}, HEX={hex_color}") - selected_scheme = self.scheme_dropdown.get_active_id() - # Run matugen with the chosen hex color and selected scheme - exec_shell_command_async(f'matugen color hex "{hex_color}" -t {selected_scheme}') - # Optionally save the chosen color to config if needed later - # config.config.bind_vars["matugen_hex_color"] = hex_color - # config.config.save_config() # Removed as save_config doesn't exist diff --git a/styles/colors.css b/styles/colors.css index 0f3590e..81f3b1c 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -1,20 +1,20 @@ :vars { - --foreground: #e3e1e9; - --background: #121318; - --cursor: #e3e1e9; - --primary: #bac3ff; - --on-primary: #222c61; - --secondary: #c3c5dd; - --on-secondary: #2c2f42; - --tertiary: #e5bad8; - --on-tertiary: #44263e; - --surface: #121318; - --surface-bright: #38393f; + --foreground: #e2e2e9; + --background: #111318; + --cursor: #e2e2e9; + --primary: #adc6ff; + --on-primary: #102f60; + --secondary: #bfc6dc; + --on-secondary: #293041; + --tertiary: #debcdf; + --on-tertiary: #402843; + --surface: #111318; + --surface-bright: #37393e; --error: #ffb4ab; --error-dim: #ff8678; --on-error: #690005; --error-container: #93000a; - --outline: #90909a; + --outline: #8e9099; --shadow: #000000; --red: #ffb2b9; --red-dim: #ff7f8b; @@ -22,8 +22,8 @@ --green-dim: #70c789; --yellow: #b8cf84; --yellow-dim: #a3c15f; - --blue: #bcc2ff; - --blue-dim: #8994ff; + --blue: #b6c4ff; + --blue-dim: #839bff; --magenta: #e4b7f3; --magenta-dim: #d48bec; --cyan: #82d3e2;