feat(update): huge changes

This commit is contained in:
sankalp committed 2025-04-25 06:33:51 +05:45
1 parent 53c077106f
commit 6b9b419aa9
98 files changed
+9636 -5575

No files matched your search

+174 -256
View File
@@ -1,33 +1,21 @@
import hashlib, json, os, colorsys
from PIL import Image
import os
import hashlib
from gi.repository import GdkPixbuf, Gtk, GLib, Gio, Gdk
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk
from fabric.utils import get_relative_path, exec_shell_command_async
from concurrent.futures import ThreadPoolExecutor, wait
from utils import WALLPAPERS_DIR
from fabric.widgets.label import Label
from fabric.utils.helpers import exec_shell_command_async
from PIL import Image
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
import utils.icons as icons
import config.data as data
class WallpaperSelector(Box):
CACHE_DIR = os.path.expanduser("~/.cache/modus/wallpapers")
SETTINGS_FILE = get_relative_path("../../../config/assets/settings.json")
CURRENT_WALLPAPER_FILE = os.path.expanduser("~/.cache/current_wallpaper")
SCHEMES = {
"TonalSpot": "tonalSpot",
"Expressive": "expressive",
"FruitSalad": "fruitSalad",
"Monochrome": "monochrome",
"Rainbow": "rainbow",
"Vibrant": "vibrant",
"Neutral": "neutral",
"Fidelity": "fidelity",
"Content": "content",
}
ITEM_WIDTH = 108
CACHE_DIR = f"{data.CACHE_DIR}/thumbnails"
def __init__(self, **kwargs):
super().__init__(
@@ -38,33 +26,44 @@ class WallpaperSelector(Box):
v_expand=False,
**kwargs,
)
self.launcher = kwargs["launcher"]
os.makedirs(self.CACHE_DIR, exist_ok=True)
self.files = [f for f in os.listdir(WALLPAPERS_DIR) if self._is_image(f)]
self.thumbnails, self.thumbnail_queue = [], []
self.executor = ThreadPoolExecutor(max_workers=4)
self.selected_index, self.current_wallpaper = -1, None
self._init_widgets()
self._start_thumbnail_thread()
self.setup_file_monitor()
if os.path.exists(self.CURRENT_WALLPAPER_FILE):
gfile = Gio.File.new_for_path(self.CURRENT_WALLPAPER_FILE)
self.current_wp_monitor = gfile.monitor_file(
Gio.FileMonitorFlags.NONE, None
)
self.current_wp_monitor.connect(
"changed", self.on_current_wallpaper_changed
)
self.show_all()
self.search_entry.grab_focus()
def _init_widgets(self):
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 possible 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(self.ITEM_WIDTH)
self.viewport.set_item_width(0)
self.viewport.connect("item-activated", self.on_wallpaper_selected)
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
@@ -72,117 +71,122 @@ class WallpaperSelector(Box):
v_expand=True,
child=self.viewport,
)
self.search_entry = Entry(
name="search-entry-walls",
placeholder="Search Wallpapers...",
h_expand=True,
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 display_name, scheme_id in sorted(self.SCHEMES.items()):
self.scheme_dropdown.append(scheme_id, display_name)
self.scheme_dropdown.set_active_id("tonalSpot")
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)
self.materialyoucolor_switcher = Gtk.Switch(name="materialyoucolor-switcher")
self.materialyoucolor_switcher.set_vexpand(False)
self.materialyoucolor_switcher.set_hexpand(False)
self.materialyoucolor_switcher.set_valign(Gtk.Align.CENTER)
self.materialyoucolor_switcher.set_halign(Gtk.Align.CENTER)
self.materialyoucolor_switcher.set_active(True)
# Create a switcher to enable/disable Matugen (enabled by default)
self.matugen_switcher = Gtk.Switch(name="matugen-switcher")
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(True)
self.mat_icon = Label(name="mat-label", markup=icons.palette)
self.dropdown_box = Box(name="dropdown-box", children=[self.scheme_dropdown])
self.custom_color_entry = Entry(
name="entry-custom-color",
h_expand=True,
notify_text=lambda entry, *_: self.on_custom_color_submitted(entry),
on_key_press_event=self.on_custom_color_key_press,
)
# Add the switcher to the header_box's start_children
self.header_box = CenterBox(
name="header-box",
spacing=10,
spacing=8,
orientation="h",
start_children=[self.materialyoucolor_switcher, self.mat_icon],
center_children=[
self.search_entry,
Label(label="Custom Color:"),
self.custom_color_entry,
],
end_children=[self.dropdown_box],
start_children=[self.matugen_switcher, self.mat_icon],
center_children=[self.search_entry],
end_children=[self.scheme_dropdown],
)
self.add(self.header_box)
self.add(self.scrolled_window)
self._start_thumbnail_thread()
self.setup_file_monitor() # Initialize file monitoring
self.show_all()
# Ensure the search entry gets focus when starting
self.search_entry.grab_focus()
def setup_file_monitor(self):
gfile = Gio.File.new_for_path(WALLPAPERS_DIR)
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_current_wallpaper_changed(self, monitor, file, other_file, event_type):
if event_type in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED):
try:
with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
new_wp = f.read().strip()
if new_wp and new_wp != self.current_wallpaper:
self.current_wallpaper = new_wp
if self.custom_color_entry.get_text().strip().lower() in [
"",
"none",
]:
scheme = self.scheme_dropdown.get_active_id()
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
command = f'python -O {color_generator} --scheme "{scheme}" --image "{self.current_wallpaper}"'
self._run_command(command)
except Exception as e:
print(f"Error updating current wallpaper: {e}")
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)
self._delete_cache(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())
return
if event_type in (
Gio.FileMonitorEvent.CREATED,
Gio.FileMonitorEvent.CHANGED,
) and self._is_image(file_name):
if (
event_type == Gio.FileMonitorEvent.CREATED
and file_name not in self.files
):
self.files.append(file_name)
self.files.sort()
elif event_type == Gio.FileMonitorEvent.CHANGED and file_name in self.files:
self._delete_cache(file_name)
self.executor.submit(self._process_file, file_name)
def _delete_cache(self, file_name: str):
cache_path = self._get_cache_path(file_name)
if os.path.exists(cache_path):
try:
os.remove(cache_path)
except Exception:
pass
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()
for pixbuf, file_name in sorted(
[
(thumb, name)
for thumb, name in self.thumbnails
if query.casefold() in name.casefold()
],
key=lambda x: x[1].lower(),
):
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 not query.strip():
# 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:
@@ -191,39 +195,40 @@ class WallpaperSelector(Box):
def on_wallpaper_selected(self, iconview, path):
model = iconview.get_model()
file_name = model[path][1]
full_path = os.path.join(WALLPAPERS_DIR, file_name)
full_path = os.path.join(data.WALLPAPERS_DIR, file_name)
selected_scheme = self.scheme_dropdown.get_active_id()
wallpaper_script = get_relative_path("../../../config/scripts/wallpaper.py")
if self.materialyoucolor_switcher.get_active():
command = f"python -O {wallpaper_script} -I {full_path}"
GLib.spawn_command_line_async(command)
self.update_scheme(selected_scheme)
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:
cmd = f"swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest"
exec_shell_command_async(cmd)
# 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):
scheme_list = [
scheme_id for _, scheme_id in sorted(self.SCHEMES.items())
]
try:
current_index = scheme_list.index(
self.scheme_dropdown.get_active_id()
)
except ValueError:
current_index = 0
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(scheme_list)
(current_index - 1) % len(schemes_list)
if event.keyval == Gdk.KEY_Up
else (current_index + 1) % len(scheme_list)
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
@@ -239,6 +244,7 @@ class WallpaperSelector(Box):
total_items = len(model)
if total_items == 0:
return
if self.selected_index == -1:
new_index = (
0 if keyval in (Gdk.KEY_Down, Gdk.KEY_Right) else total_items - 1
@@ -246,39 +252,45 @@ class WallpaperSelector(Box):
else:
current_index = self.selected_index
allocation = self.viewport.get_allocation()
columns = max(1, allocation.width // self.ITEM_WIDTH)
new_index = current_index + (
1
if keyval == Gdk.KEY_Right
else -1
if keyval == Gdk.KEY_Left
else columns
if keyval == Gdk.KEY_Down
else -columns
)
new_index = max(0, min(new_index, total_items - 1))
item_width = 108 # Approximate item width including margins
columns = max(1, allocation.width // item_width)
if keyval == Gdk.KEY_Right:
new_index = current_index + 1
elif keyval == Gdk.KEY_Left:
new_index = current_index - 1
elif keyval == Gdk.KEY_Down:
new_index = current_index + columns
elif keyval == Gdk.KEY_Up:
new_index = current_index - columns
if new_index < 0:
new_index = 0
if new_index >= total_items:
new_index = total_items - 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)
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):
GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
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
]
wait(futures)
concurrent.futures.wait(futures)
GLib.idle_add(self._process_batch)
def _process_file(self, file_name):
full_path = os.path.join(WALLPAPERS_DIR, 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:
@@ -287,10 +299,13 @@ class WallpaperSelector(Box):
side = min(width, height)
left = (width - side) // 2
top = (height - side) // 2
img_cropped = img.crop((left, top, left + side, top + side))
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:
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)
@@ -313,110 +328,13 @@ class WallpaperSelector(Box):
file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
def on_scheme_changed(self, combo):
scheme_id = combo.get_active_id()
display_name = next(
name for name, id in self.SCHEMES.items() if id == scheme_id
)
self.update_scheme(scheme_id)
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
self._run_command(
command,
success_message=f"Applied color scheme: {display_name}",
failure_message="Failed to apply color scheme:",
)
def update_scheme(self, scheme: str):
self._update_settings_field("generation-scheme", scheme)
def on_custom_color_submitted(self, entry: Entry):
color = entry.get_text().strip()
if not color:
self.update_custom_color("")
return
if self.is_valid_hex_color(color):
color_value = color
elif self.is_valid_hue(color):
color_value = self.hue_to_hex(float(color))
else:
return
self.update_custom_color(color_value)
def on_custom_color_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Return:
self.on_custom_color_submitted(widget)
return True
return False
def _fetch_current_wallpaper(self):
if not self.current_wallpaper or not os.path.exists(self.current_wallpaper):
if os.path.exists(self.CURRENT_WALLPAPER_FILE):
try:
with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
self.current_wallpaper = f.read().strip()
except Exception as e:
print(f"Error reading current wallpaper: {e}")
return self.current_wallpaper
def update_custom_color(self, color: str):
color = color if color and color.lower() != "none" else "none"
self._update_settings_field("custom-color", color)
color_generator = get_relative_path(
"../../../config/material-colors/generate.py"
)
if color == "none":
scheme = self.scheme_dropdown.get_active_id()
current_wp = self._fetch_current_wallpaper()
command = f'python -O {color_generator} --scheme "{scheme}" --image "{current_wp}"'
else:
command = f'python -O {color_generator} --color "{color}"'
self._run_command(command)
@staticmethod
def _is_image(file_name: str) -> bool:
return file_name.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
@staticmethod
def is_valid_hex_color(value: str) -> bool:
return (
value.startswith("#")
and len(value) == 7
and all(c in "0123456789ABCDEFabcdef" for c in value[1:])
)
@staticmethod
def is_valid_hue(value: str) -> bool:
try:
return 0 <= float(value) <= 360
except ValueError:
return False
def hue_to_hex(self, hue: float) -> str:
r, g, b = colorsys.hls_to_rgb(hue / 360.0, 0.5, 1.0)
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
def _update_settings_field(self, field: str, value):
try:
with open(self.SETTINGS_FILE, "r") as f:
settings = json.load(f)
settings[field] = value
with open(self.SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
except Exception:
pass
def _run_command(
self, command: str, success_message: str = None, failure_message: str = None
):
try:
GLib.spawn_command_line_async(command)
if success_message:
print(success_message)
except Exception:
if failure_message:
print(failure_message)
def on_search_entry_focus_out(self, widget, event):
if self.get_mapped():
widget.grab_focus()
return False