💅 style(colors): update color scheme for better contrast and consistency
This commit is contained in:
6 files changed
+992
-576
No files matched your search
+11
-11
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+14
-14
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user