From 24935c925d98158949241d47d15338408dce19e4 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:44:38 +0545 Subject: [PATCH] huge updates but idk what i update --- modules/bar/bar.py | 184 ++++++--- modules/bar/components/__init__.py | 5 +- modules/bar/components/battery.py | 8 +- .../bar/components/{stats.py => metric.py} | 30 +- .../{indicators.py => system_indicators.py} | 20 +- modules/bar/components/taskbar.py | 154 -------- modules/bar/components/updates.py | 4 +- modules/launcher/components/applauncher.py | 320 ++++++--------- modules/launcher/components/bluetooth.py | 2 +- modules/launcher/components/calendar.py | 6 +- modules/launcher/components/cliphist.py | 30 +- modules/launcher/components/dashboard.py | 2 +- modules/launcher/components/emoji.py | 26 +- modules/launcher/components/power.py | 3 +- modules/launcher/components/sh.py | 2 +- modules/launcher/components/todo.py | 4 +- modules/launcher/components/wallpapers.py | 368 +++++++++++------- modules/launcher/components/wifi.py | 2 +- modules/launcher/launcher.py | 10 +- 19 files changed, 563 insertions(+), 617 deletions(-) rename modules/bar/components/{stats.py => metric.py} (77%) rename modules/bar/components/{indicators.py => system_indicators.py} (88%) delete mode 100644 modules/bar/components/taskbar.py diff --git a/modules/bar/bar.py b/modules/bar/bar.py index dc91a6e..278e228 100644 --- a/modules/bar/bar.py +++ b/modules/bar/bar.py @@ -1,3 +1,4 @@ +from typing import Literal from fabric.hyprland.widgets import Language from fabric.system_tray.widgets import SystemTray from fabric.utils import ( @@ -5,24 +6,76 @@ from fabric.utils import ( bulk_replace, exec_shell_command_async, get_relative_path, + bulk_connect, ) +from fabric.widgets.shapes import Corner from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label +from fabric.widgets.revealer import Revealer from fabric.widgets.wayland import WaylandWindow as Window from gi.repository import GLib from modules.bar.components import ( - BatteryLabel, - SystemInfo, - TaskBar, + Battery, + Metrics, workspace, - Indicators, + SystemIndicators, UpdatesWidget, ) from services import sc -import snippets.iconss as icons +import utils.icons as icons + + +class StatusBarCorner(Box): + def __init__(self, corner: Literal["top-right", "top-left"]): + super().__init__( + style="margin-bottom: 15px;", + name="bar-corner", + children=Corner( + orientation=corner, + size=15, + ), + ) + + +class Tray(Box): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.tray = Box( + name="left-tray", + children=[SystemTray(name="tray", icon_size=16, spacing=4)], + ) + self.profile = Button( + child=Label(name="tray-revealer", markup=icons.chevron_left) + ) + self.revealer = Revealer(transition_type="slide-left", transition_duration=1000) + self.revealer.add(self.tray) + + bulk_connect( + self.profile, + { + "enter-notify-event": self._on_enter, + "leave-notify-event": self._on_leave, + "button-press-event": self.toggle_revealer, + }, + ) + + self.add(self.profile) + self.add(self.revealer) + + def _on_enter(self, *args): + self.profile.set_cursor("pointer") + + def _on_leave(self, *args): + self.profile.set_cursor("default") + + def toggle_revealer(self, *args): + new_state = not self.revealer.get_reveal_child() + self.revealer.set_reveal_child(new_state) + new_icon = icons.chevron_right if new_state else icons.chevron_left + self.profile.get_child().set_markup(new_icon) class Bar(Window): @@ -39,7 +92,7 @@ class Bar(Window): ), ), ) - + self.bar_content = CenterBox(name="center-bar") self.recording_indicator = Button( name="recording-indicator", child=Label(name="recorder", markup=icons.record), @@ -47,16 +100,10 @@ class Bar(Window): on_clicked=lambda *_: sc.screencast_stop(), ) - sc.connect( - "recording", lambda _, status: self.on_recording_status_change(status) - ) + sc.connect("recording", self.on_recording_status_change) self.date_time = DateTime(formatters=["%-I:%M 󰧞 %a %d %b"], name="datetime") - - self.battery = BatteryLabel() - self.taskbar = TaskBar() - self.stats = SystemInfo() - self.tray = SystemTray(name="tray", icon_size=16, spacing=4) + self.battery = Battery() self.launcher = Button( name="logo", child=Label(name="logo-name", label="󰣇 󰫿󰫰󰫵"), @@ -64,8 +111,6 @@ class Bar(Window): "fabric-cli exec modus 'launcher.open(\"launcher\")'" ), ) - self.updates = UpdatesWidget() - self.indicators = Indicators() self.button_config = Button( name="button-bar", on_clicked=lambda *_: exec_shell_command_async( @@ -73,35 +118,22 @@ class Bar(Window): ), child=Label(name="button-bar-label", markup=icons.config), ) - + self.stats = Metrics() + self.updates = UpdatesWidget() + self.tray = Tray() + self.indicators = SystemIndicators() self.applets = Box( - name="applets", + name="system-indicators", spacing=4, orientation="h", - children=[ - self.language, - self.indicators, - ], + children=[self.language, self.indicators], ) - self.bar = CenterBox( - name="bar", - start_children=Box( - name="start-container", - spacing=8, - orientation="h", - children=[self.launcher, self.workspaces, self.stats, self.updates], - ), - center_children=Box( - name="center-container", - spacing=8, - orientation="h", - children=self.taskbar, - ), - end_children=Box( - name="end-container", - spacing=8, - orientation="h", + self.bar_content.end_children = [ + StatusBarCorner("top-right"), + Box( + name="bar", + spacing=4, children=[ self.recording_indicator, self.tray, @@ -110,27 +142,79 @@ class Bar(Window): self.date_time, self.button_config, ], + style_classes="end-container", ), - ) + ] + + self.bar_content.start_children = [ + Box( + name="bar", + spacing=4, + children=[self.launcher, self.workspaces, self.stats, self.updates], + style_classes="start-container", + ), + StatusBarCorner("top-left"), + ] + super().__init__( layer="top", - anchor="left bottom right", + anchor="left top right", exclusivity="auto", visible=True, - child=self.bar, + child=self.bar_content, ) - self.hidden = False - def on_recording_status_change(self, status): - print(f"Recording status changed: {status}") + def on_recording_status_change(self, _, status): self.recording_indicator.set_visible(status) def toggle_hidden(self): self.hidden = not self.hidden if self.hidden: - self.bar.remove_style_class("visible") - self.bar.add_style_class("hidden") + self.bar_content.add_style_class("hidden") + self.bar_content.remove_style_class("visible") + self.set_property("visible", False) else: - self.bar.remove_style_class("hidden") - self.bar.add_style_class("visible") + self.bar_content.add_style_class("visible") + self.bar_content.remove_style_class("hidden") + self.set_property("visible", True) + + +class ScreenCorners(Window): + def __init__(self): + super().__init__( + layer="top", + anchor="top left bottom right", + pass_through=True, + child=Box( + orientation="vertical", + children=[ + Box( + children=[ + self.make_corner("top-left"), + Box(h_expand=True), + self.make_corner("top-right"), + ] + ), + Box(v_expand=True), + Box( + children=[ + self.make_corner("bottom-left"), + Box(h_expand=True), + self.make_corner("bottom-right"), + ] + ), + ], + ), + ) + + def make_corner(self, orientation) -> Box: + return Box( + h_expand=False, + v_expand=False, + name="bar-corner", + children=Corner( + orientation=orientation, + size=15, + ), + ) diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py index 116ed6d..4cba446 100644 --- a/modules/bar/components/__init__.py +++ b/modules/bar/components/__init__.py @@ -1,6 +1,5 @@ from .battery import * -from .stats import * -from .taskbar import * +from .metric import * from .workspace import * -from .indicators import * +from .system_indicators import * from .updates import * diff --git a/modules/bar/components/battery.py b/modules/bar/components/battery.py index 60440b6..5707911 100644 --- a/modules/bar/components/battery.py +++ b/modules/bar/components/battery.py @@ -10,10 +10,10 @@ from fabric.widgets.revealer import Revealer from fabric.core.fabricator import Fabricator from fabric.utils.helpers import exec_shell_command_async from gi.repository import GLib -import snippets.iconss as icons +import utils.icons as icons -class BatteryLabel(Box): +class Battery(Box): def __init__(self, **kwargs): super().__init__( name="battery", @@ -59,8 +59,8 @@ class BatteryLabel(Box): value=0, size=28, line_width=2, - start_angle=180, - end_angle=360, + start_angle=150, + end_angle=390, ) self.bat_overlay = Overlay( diff --git a/modules/bar/components/stats.py b/modules/bar/components/metric.py similarity index 77% rename from modules/bar/components/stats.py rename to modules/bar/components/metric.py index 9b5c5ab..d0e026a 100644 --- a/modules/bar/components/stats.py +++ b/modules/bar/components/metric.py @@ -5,11 +5,10 @@ from fabric.widgets.label import Label from fabric.widgets.box import Box from fabric.widgets.overlay import Overlay from fabric.widgets.circularprogressbar import CircularProgressBar - -import snippets.iconss as icons +import utils.icons as icons -class SystemInfo(Box): +class Metrics(Box): ICONS = { "CPU": icons.cpu, "RAM": icons.memory, @@ -20,7 +19,7 @@ class SystemInfo(Box): def __init__(self, **kwargs): super().__init__( - name="system-info", + name="metrics", spacing=8, h_align="center", v_align="fill", @@ -30,14 +29,12 @@ class SystemInfo(Box): self.progress_bars = { system: CircularProgressBar( - name="stats-progress", + name="metric-circle", line_style="round", - line_width=3, + line_width=2, size=28, - start_at=0, - end_at=0.5, - start_angle=0, - end_angle=180, + start_angle=150, + end_angle=390, ) for system in self.ICONS } @@ -45,7 +42,7 @@ class SystemInfo(Box): for system, icon_name in self.ICONS.items(): overlay = Overlay( child=self.progress_bars[system], - overlays=[Label(name="stats-icons", markup=icon_name)], + overlays=[Label(name="metric-icon", markup=icon_name)], ) self.add(overlay) @@ -55,14 +52,17 @@ class SystemInfo(Box): ram_usage = psutil.virtual_memory().percent swap_usage = psutil.swap_memory().percent disk_usage = psutil.disk_usage("/").percent - cpu = psutil.cpu_percent(interval=0) + cpu_usage = psutil.cpu_percent(interval=0) + + temp = self._get_device_temperature() + temp_usage = temp if temp is not None else 0 usages = { - "CPU": cpu, + "CPU": cpu_usage, "RAM": ram_usage, "Swap": swap_usage, "Disk": disk_usage, - "Temp": self._get_device_temperature(), + "Temp": temp_usage, } for system, usage in usages.items(): @@ -76,7 +76,7 @@ class SystemInfo(Box): try: temps = psutil.sensors_temperatures() for key in ("coretemp", "cpu_thermal"): - if key in temps: + if key in temps and temps[key]: return round(temps[key][0].current, 1) except Exception: pass diff --git a/modules/bar/components/indicators.py b/modules/bar/components/system_indicators.py similarity index 88% rename from modules/bar/components/indicators.py rename to modules/bar/components/system_indicators.py index 5bf8fe8..0f49299 100644 --- a/modules/bar/components/indicators.py +++ b/modules/bar/components/system_indicators.py @@ -4,19 +4,19 @@ from fabric.widgets.label import Label from fabric.widgets.box import Box from fabric.bluetooth import BluetoothClient from services import NetworkClient, audio -import snippets.iconss as icons +import utils.icons as icons -class Indicators(Box): +class SystemIndicators(Box): def __init__(self, **kwargs): super().__init__(orientation="h", spacing=2, **kwargs) - self.bluetooth_icon = Label(name="indicator-icon") - self.wifi_icon = Label(name="indicator-icon") - self.volume_icon_button = Label(name="indicator-icon") - self.microphone_icon = Label(name="indicator-icon") - self.idle_label = Label(name="indicator-icon") - self.night_label = Label(name="indicator-icon") + self.bluetooth_icon = Label(name="system-indicator-icon") + self.wifi_icon = Label(name="system-indicator-icon") + self.volume_icon_button = Label(name="system-indicator-icon") + self.microphone_icon = Label(name="system-indicator-icon") + self.idle_label = Label(name="system-indicator-icon") + self.night_label = Label(name="system-indicator-icon") for widget in [ self.bluetooth_icon, @@ -86,8 +86,8 @@ class Indicators(Box): if stream.muted else ( icons.vol_mute - if volume_level == 0 - else (icons.vol_medium if volume_level <= 50 else icons.vol_high) + if volume_level < 1 + else (icons.vol_medium if volume_level >= 1 else icons.vol_high) ) ) diff --git a/modules/bar/components/taskbar.py b/modules/bar/components/taskbar.py deleted file mode 100644 index b54d0bb..0000000 --- a/modules/bar/components/taskbar.py +++ /dev/null @@ -1,154 +0,0 @@ -import json -import os -from typing import TypedDict - -from fabric.hyprland.widgets import get_hyprland_connection -from fabric.utils import exec_shell_command_async -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.image import Image -from gi.repository import GdkPixbuf, GLib, Gtk -from loguru import logger - - -class PagerClient(TypedDict): - title: str - initialClass: str - mapped: bool - hidden: bool - address: str - - -class TaskBar(Box): - def __init__(self, icon_size: int = 22, **kwargs): - super().__init__( - orientation="h", - spacing=7, - name="taskbar", - **kwargs, - ) - self.connection = get_hyprland_connection() - self.icon_size = icon_size - self.icon_theme = Gtk.IconTheme.get_default() - - self.set_visible(False) - - if self.connection.ready: - self.render_with_delay() - else: - self.connection.connect("event::ready", lambda *_: self.render_with_delay()) - - for event in ( - "activewindow", - "openwindow", - "closewindow", - "changefloatingmode", - ): - self.connection.connect("event::" + event, lambda *_: self.render(None)) - - def render_with_delay(self, *_): - GLib.timeout_add(101, self.render) - - def render(self, *_): - self.children = [] - - clients = self.fetch_clients() - active_window_address = self.get_active_window_address() - - visible_clients = [ - client for client in clients if client["mapped"] and not client["hidden"] - ] - - if visible_clients: - for client in visible_clients: - window_class = client["initialClass"].lower() - icon = self.bake_window_icon(window_class) - - button = Button(image=icon, tooltip_text=client["title"]) - if client["address"] == active_window_address: - button.add_style_class("activated") - button.connect( - "button-press-event", - self.on_icon_click, - client["address"], - ) - - self.add(button) - self.set_visible(True) - self.show_all() - else: - self.set_visible(False) - - self.set_visible(len(self.children) > 0) - if self.children: - self.show_all() - - def get_active_window_address(self) -> str: - active_window_info = json.loads( - self.connection.send_command("j/activewindow").reply.decode() - ) - return active_window_info.get("address", "") - - def on_icon_click(self, widget, event, address): - exec_shell_command_async( - f"hyprctl dispatch focuswindow address:{address}", lambda *_: None - ) - - def fetch_clients(self) -> list[PagerClient]: - return json.loads(self.connection.send_command("j/clients").reply.decode()) - - def bake_window_icon( - self, - window_class: str, - fallback_icon: str = "image-missing", - ) -> Image: - icon_name = self.get_icon_from_desktop_entry(window_class) - - if icon_name: - pixbuf = self.load_icon(icon_name) - else: - pixbuf = self.load_icon(window_class, fallback_icon) - - return Image(pixbuf=pixbuf, size=self.icon_size) - - def get_icon_from_desktop_entry(self, window_class: str) -> str: - for data_dir in GLib.get_system_data_dirs(): - applications_dir = os.path.join(data_dir, "applications") - - if os.path.isdir(applications_dir): - for desktop_file in os.listdir(applications_dir): - if desktop_file.endswith(".desktop"): - file_path = os.path.join(applications_dir, desktop_file) - try: - with open(file_path) as f: - app_name = None - icon_name = None - for line in f: - if line.startswith("Name="): - app_name = line.split("=", 2)[1].strip().lower() - elif line.startswith("Icon=") and app_name: - icon_name = line.split("=", 2)[1].strip() - if window_class in app_name: - return icon_name - except Exception as e: - logger.error(f"Error reading {file_path}: {e}") - return None - - def load_icon( - self, - icon_name: str, - fallback_icon: str = "image-missing", - ) -> GdkPixbuf.Pixbuf: - try: - pixbuf = self.icon_theme.load_icon( - icon_name, - self.icon_size, - Gtk.IconLookupFlags.FORCE_SIZE, - ) - except Exception: - pixbuf = self.icon_theme.load_icon( - fallback_icon, - self.icon_size, - Gtk.IconLookupFlags.FORCE_SIZE, - ) - return pixbuf diff --git a/modules/bar/components/updates.py b/modules/bar/components/updates.py index 0674d3e..a285479 100644 --- a/modules/bar/components/updates.py +++ b/modules/bar/components/updates.py @@ -3,11 +3,11 @@ from fabric.utils import exec_shell_command_async, get_relative_path, invoke_rep from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label +from fabric.widgets.button import Button from fabric.widgets.stack import Stack -import snippets.iconss as icons from loguru import logger from gi.repository import Gtk -from fabric.widgets.button import Button +import utils.icons as icons class UpdatesWidget(Button): diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py index 69669b3..fb9ce05 100644 --- a/modules/launcher/components/applauncher.py +++ b/modules/launcher/components/applauncher.py @@ -1,62 +1,49 @@ from collections.abc import Iterator -import operator from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler 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 GLib, Gdk -from snippets import read_config from fabric.widgets.image import Image +from gi.repository import Gdk, GLib import json import os -import re import math +import re import subprocess import webbrowser +from functools import partial +from utils import read_config class AppLauncher(Box): def __init__(self, **kwargs): super().__init__( - name="app-launcher", - visible=False, - all_visible=False, - **kwargs, + name="app-launcher", visible=False, all_visible=False, **kwargs ) + self.scrolled_window = None self.launcher = kwargs["launcher"] self.selected_index = -1 - self._arranger_handler: int = 0 + self._arranger_handler = 0 self._all_apps = get_desktop_applications() self.config = read_config() - # Calculator history initialization self.calc_history_path = os.path.expanduser("~/.cache/modus/calc.json") - if os.path.exists(self.calc_history_path): - with open(self.calc_history_path, "r") as f: - self.calc_history = json.load(f) - else: - self.calc_history = [] + self.calc_history = self._load_calc_history() self.viewport = Box(name="viewport", spacing=4, orientation="v") self.search_entry = Entry( name="search-entry", h_expand=True, - notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()), - on_activate=lambda entry, *_: self.on_search_entry_activate( - entry.get_text() - ), + notify_text=self._on_notify_text, + on_activate=self._on_activate, on_key_press_event=self.on_search_entry_key_press, ) - self.scrolled_window = None - self.header_box = Box( - name="header-box", - orientation="h", - children=[self.search_entry], + name="header-box", orientation="h", children=[self.search_entry] ) self.launcher_box = Box( @@ -69,28 +56,43 @@ class AppLauncher(Box): self.add(self.launcher_box) self.show_all() + def _load_calc_history(self): + if os.path.exists(self.calc_history_path): + with open(self.calc_history_path, "r") as f: + return json.load(f) + return [] + + def _on_notify_text(self, entry, *args): + self.arrange_viewport(entry.get_text()) + + def _on_activate(self, entry, *args): + self.on_search_entry_activate(entry.get_text()) + def close_launcher(self, *_): self.viewport.children = [] self.selected_index = -1 self.launcher.close() + self.hide_scrolled_window() def open_launcher(self): self._all_apps = get_desktop_applications() self.arrange_viewport("") def arrange_viewport(self, query: str = ""): - if query.startswith(":"): # Don't show ScrolledWindow for web search - self.destroy_scrolled_window() + if query.startswith(":"): + self.hide_scrolled_window() self.launcher.dashboard.hide() return + if query.startswith("="): self.show_scrolled_window() - # In calculator mode, update history view once (not per keystroke) self.update_calculator_viewport() self.launcher.dashboard.hide() return - remove_handler(self._arranger_handler) if self._arranger_handler else None + if self._arranger_handler: + remove_handler(self._arranger_handler) + self._arranger_handler = 0 self.viewport.children = [] self.selected_index = -1 @@ -99,171 +101,140 @@ class AppLauncher(Box): self.show_scrolled_window() else: self.launcher.dashboard.show() - self.destroy_scrolled_window() + self.hide_scrolled_window() return False + filtered_apps = [ + app + for app in self._all_apps + if query.casefold() + in ( + (app.display_name or "") + + " " + + app.name + + " " + + (app.generic_name or "") + ).casefold() + ] filtered_apps_iter = iter( - sorted( - [ - app - for app in self._all_apps - if query.casefold() - in ( - (app.display_name or "") - + (" " + app.name + " ") - + (app.generic_name or "") - ).casefold() - ], - key=lambda app: (app.display_name or "").casefold(), - ) + sorted(filtered_apps, key=lambda app: (app.display_name or "").casefold()) ) - should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps) self._arranger_handler = idle_add( - lambda apps_iter: self.add_next_application(apps_iter) - or self.handle_arrange_complete(should_resize, query), - filtered_apps_iter, - pin=True, + partial(self._arranger_callback, query=query), filtered_apps_iter, pin=True ) + def _arranger_callback(self, apps_iter: Iterator[DesktopApp], query: str): + if self.add_next_application(apps_iter): + return True + return self.handle_arrange_complete(query) + def show_scrolled_window(self): if self.scrolled_window is None: self.scrolled_window = ScrolledWindow( name="scrolled-window", - spacing=10, - min_content_size=(-1, -1), + style_classes="applauncher-scrolled-window", h_scrollbar_policy="never", child=self.viewport, - v_expand=True, ) self.launcher_box.add(self.scrolled_window) - self.scrolled_window.show_all() - self.get_style_context().add_class("applauncher") - def destroy_scrolled_window(self): - if hasattr(self, "scrolled_window") and self.scrolled_window: - # self.scrolled_window.destroy() - self.launcher_box.remove(self.scrolled_window) - self.scrolled_window = None + self.scrolled_window.show_all() + GLib.idle_add(self.update_scrolled_window_size) - self.get_style_context().remove_class("applauncher") + def hide_scrolled_window(self): + if self.scrolled_window: + self.scrolled_window.hide() - def handle_arrange_complete(self, should_resize, query): - if should_resize: - self.resize_viewport() - # Only auto-select first item if query exists - if query.strip() != "" and self.viewport.get_children(): + def handle_arrange_complete(self, query): + self.update_scrolled_window_size() + if query.strip() and self.viewport.get_children(): self.update_selection(0) return False def add_next_application(self, apps_iter: Iterator[DesktopApp]): - if not (app := next(apps_iter, None)): + app = next(apps_iter, None) + if app is None: return False - self.viewport.add(self.bake_application_slot(app)) return True - def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button: - button = Button( + def bake_application_slot(self, app: DesktopApp) -> Button: + return Button( name="launcher-app", child=Box( orientation="h", children=[ - Image( - pixbuf=app.get_icon_pixbuf(size=32), - h_align="start", - name="launcher-app-icon", - ), + Image(pixbuf=app.get_icon_pixbuf(size=32), h_align="start"), Label( label=app.display_name or "Unknown", ellipsization="end", v_align="center", - h_align="center", ), ], ), tooltip_text=app.description, - on_clicked=lambda *_: (app.launch(), self.close_launcher()), - **kwargs, + on_clicked=partial(self._on_app_clicked, app), ) - return button + + def _on_app_clicked(self, app, *args): + app.launch() + self.close_launcher() def update_selection(self, new_index: int): - if self.selected_index != -1 and self.selected_index < len( - self.viewport.get_children() - ): - current_button = self.viewport.get_children()[self.selected_index] - current_button.get_style_context().remove_class("selected") + children = self.viewport.get_children() + if self.selected_index >= 0 < len(children): + children[self.selected_index].get_style_context().remove_class("selected") - if new_index != -1 and new_index < len(self.viewport.get_children()): - new_button = self.viewport.get_children()[new_index] - new_button.get_style_context().add_class("selected") + if 0 <= new_index < len(children): + children[new_index].get_style_context().add_class("selected") self.selected_index = new_index - self.scroll_to_selected(new_button) else: self.selected_index = -1 - def scroll_to_selected(self, button): - def scroll(): - adj = self.scrolled_window.get_vadjustment() - alloc = button.get_allocation() - if alloc.height == 0: - return False + def update_scrolled_window_size(self): + if self.scrolled_window is None: + return - y = alloc.y - height = alloc.height - page_size = adj.get_page_size() - current_value = adj.get_value() + children = self.viewport.get_children() + item_count = len(children) - visible_top = current_value - visible_bottom = current_value + page_size + if item_count == 0: + self.scrolled_window.set_size_request(-1, 0) + return - if y < visible_top: - adj.set_value(y) - elif y + height > visible_bottom: - new_value = y + height - page_size - adj.set_value(new_value) - return False + first_child = children[0] + first_child_height = first_child.get_allocation().height - GLib.idle_add(scroll) + if first_child_height == 0: + GLib.idle_add(self.update_scrolled_window_size) + return + + spacing = self.viewport.props.spacing + total_height = (item_count * first_child_height) + ((item_count - 1) * spacing) + + max_height = 400 + self.scrolled_window.set_size_request(-1, min(total_height, max_height)) def on_search_entry_activate(self, text): - commands = self.config.get("commands", {}) - if text.startswith(":i "): # Detecting ':i' search - search_query = text[3:].strip() # Extract query after ':i ' - if search_query: - url = f"https://www.google.com/search?q={search_query}" - webbrowser.open(url) - self.close_launcher() + if text.startswith(":i "): + webbrowser.open(f"https://www.google.com/search?q={text[3:].strip()}") + self.close_launcher() return if text.startswith("="): - # If in calculator mode and no history item is selected, evaluate new expression. - if self.selected_index == -1: - self.evaluate_calculator_expression(text) + self.evaluate_calculator_expression(text) return - if text in commands: - command = commands[text] - exec(command, {"launcher": self.launcher}) - else: - children = self.viewport.get_children() - if children: - if text.strip() == "" and self.selected_index == -1: - return - selected_index = self.selected_index if self.selected_index != -1 else 0 - if 0 <= selected_index < len(children): - children[selected_index].clicked() + children = self.viewport.get_children() + if children: + (children[self.selected_index or 0]).clicked() def on_search_entry_key_press(self, widget, event): - keyval = event.keyval - if keyval == Gdk.KEY_Down: - self.move_selection(1) + if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Up): + self.move_selection(1 if event.keyval == Gdk.KEY_Down else -1) return True - elif keyval == Gdk.KEY_Up: - self.move_selection(-1) - return True - elif keyval == Gdk.KEY_Escape: + if event.keyval == Gdk.KEY_Escape: self.close_launcher() return True return False @@ -272,81 +243,32 @@ class AppLauncher(Box): children = self.viewport.get_children() if not children: return - if self.selected_index == -1 and delta == 1: - new_index = 0 - else: - new_index = self.selected_index + delta - new_index = max(0, min(new_index, len(children) - 1)) + new_index = max(0, min(self.selected_index + delta, len(children) - 1)) self.update_selection(new_index) - def save_calc_history(self): - with open(self.calc_history_path, "w") as f: - json.dump(self.calc_history, f) - def evaluate_calculator_expression(self, text: str): - # Remove the '=' prefix and extra spaces - expr = text.lstrip("=").strip() - if not expr: - return - # Replace operators: '^' -> '**', and '×' -> '*' - expr = expr.replace("^", "**").replace("×", "*") - # Replace factorial: e.g. 5! -> math.factorial(5) + expr = text.lstrip("=").replace("^", "**").replace("×", "*") expr = re.sub(r"(\d+)!", r"math.factorial(\1)", expr) - # Replace brackets: allow [] and {} as () - for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]: - expr = expr.replace(old, new) - try: - result = eval(expr, {"__builtins__": None, "math": math}) - except Exception as e: - result = f"Error: {e}" - # Prepend to history (newest first) + result = eval(expr, {"__builtins__": None, "math": math}) self.calc_history.insert(0, f"{text} => {result}") self.save_calc_history() self.update_calculator_viewport() def update_calculator_viewport(self): - self.viewport.children = [] - for item in self.calc_history: - btn = self.create_calc_history_button(item) - self.viewport.add(btn) - # Remove resetting selected_index unconditionally so that a highlighted result isn't lost. - # Optionally, only reset if the input is not more than "=". - # if self.search_entry.get_text().strip() != "=": - # self.selected_index = -1 + self.viewport.children = [ + Button( + child=Label(label=item), + on_clicked=partial(self.copy_text_to_clipboard, item), + ) + for item in self.calc_history + ] + self.update_scrolled_window_size() - def create_calc_history_button(self, text: str) -> Button: - btn = Button( - name="app-slot-button", # reuse existing CSS styling - child=Box( - name="calc-slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="calc-label", - label=text, - ellipsization="end", - v_align="center", - h_align="center", - ), - ], - ), - tooltip_text=text, - on_clicked=lambda *_: self.copy_text_to_clipboard(text), + def save_calc_history(self): + with open(self.calc_history_path, "w") as f: + json.dump(self.calc_history, f) + + def copy_text_to_clipboard(self, text: str, *args): + subprocess.run( + ["wl-copy"], input=text.split("=>", 1)[-1].strip().encode(), check=True ) - return btn - - def copy_text_to_clipboard(self, text: str): - # Split the text on "=>" and copy only the result part if available - parts = text.split("=>", 1) - copy_text = parts[1].strip() if len(parts) > 1 else text - try: - subprocess.run(["wl-copy"], input=copy_text.encode(), check=True) - except subprocess.CalledProcessError as e: - print(f"Clipboard copy failed: {e}") - - def delete_selected_calc_history(self): - if self.selected_index != -1 and self.selected_index < len(self.calc_history): - del self.calc_history[self.selected_index] - self.save_calc_history() - self.update_calculator_viewport() diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py index a61849a..69bf2a8 100644 --- a/modules/launcher/components/bluetooth.py +++ b/modules/launcher/components/bluetooth.py @@ -5,7 +5,7 @@ from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.bluetooth import BluetoothClient, BluetoothDevice -import snippets.iconss as icons +import utils.icons as icons class BluetoothDeviceSlot(CenterBox): diff --git a/modules/launcher/components/calendar.py b/modules/launcher/components/calendar.py index cdb2562..76bd139 100644 --- a/modules/launcher/components/calendar.py +++ b/modules/launcher/components/calendar.py @@ -5,7 +5,7 @@ from fabric.widgets.label import Label gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib -import snippets.iconss as icons +import utils.icons as icons class Calendar(Gtk.Box): @@ -27,7 +27,7 @@ class Calendar(Gtk.Box): self.prev_month_button = Gtk.Button( name="prev-month-button", - child=Label(name="month-button-label", markup=icons.chevron_left) + child=Label(name="month-button-label", markup=icons.chevron_left), ) self.prev_month_button.connect("clicked", self.on_prev_month_clicked) self.header.pack_start(self.prev_month_button, False, False, 0) @@ -37,7 +37,7 @@ class Calendar(Gtk.Box): self.next_month_button = Gtk.Button( name="next-month-button", - child=Label(name="month-button-label", markup=icons.chevron_right) + child=Label(name="month-button-label", markup=icons.chevron_right), ) self.next_month_button.connect("clicked", self.on_next_month_clicked) self.header.pack_start(self.next_month_button, False, False, 0) diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py index 86fe079..f2e66fd 100644 --- a/modules/launcher/components/cliphist.py +++ b/modules/launcher/components/cliphist.py @@ -1,6 +1,7 @@ import os import subprocess from typing import List +from functools import partial from fabric import Fabricator from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -8,7 +9,7 @@ from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import GLib -import snippets.iconss as icons +import utils.icons as icons class Cliphist(Box): @@ -19,15 +20,15 @@ class Cliphist(Box): visible=False, **kwargs, ) - self._arranger_handler = 0 self.cliphist_manager = CliphistManager(self) self.viewport = None + self.launcher = kwargs["launcher"] self.search_entry = Entry( name="search-entry", h_expand=True, - on_activate=lambda entry, *_: self.handle_search(entry.get_text()), + on_activate=self._on_search_activate, ) self.header_box = Box( @@ -55,6 +56,9 @@ class Cliphist(Box): self.add(self.launcher_box) + def _on_search_activate(self, entry, *args): + self.handle_search(entry.get_text()) + def open_launcher(self): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") @@ -63,7 +67,6 @@ class Cliphist(Box): spacing=10, min_content_size=(-1, -1), h_scrollbar_policy="never", - # v_scrollbar_policy="never", child=self.viewport, ) self.launcher_box.add(self.scrolled_window) @@ -89,9 +92,10 @@ class CliphistManager: self.wl_paste_watcher = Fabricator( poll_from="wl-paste --watch echo", stream=True, interval=-1 ) - self.wl_paste_watcher.connect( - "changed", lambda *_: self.update_cliphist_history() - ) + self.wl_paste_watcher.connect("changed", self._on_wl_paste_changed) + + def _on_wl_paste_changed(self, *args): + self.update_cliphist_history() def get_clip_history(self) -> List[dict]: try: @@ -118,14 +122,13 @@ class CliphistManager: ["cliphist", "decode", clip_id], capture_output=True, check=True ) subprocess.run(["wl-copy"], input=decode_result.stdout, check=True) - self.launcher.open_launcher() + self.launcher.close() except subprocess.CalledProcessError as e: print(f"Error copying clip {clip_id}: {e}") def save_image_file(self, clip_id: str) -> str: output_file = f"/tmp/cliphist-{clip_id}.png" os.makedirs("/tmp", exist_ok=True) - try: decode_result = subprocess.run( ["cliphist", "decode", clip_id], capture_output=True, check=True @@ -150,6 +153,9 @@ class CliphistManager: if query.lower() in clip["content"].lower() ] + def _on_clip_clicked(self, clip_id: str, *args): + self.copy_clip(clip_id) + def bake_clip_slot(self, item: dict) -> Button: if "[[ binary data" in item["content"].lower(): return self._create_image_button(item) @@ -161,7 +167,7 @@ class CliphistManager: button = Button( h_align="start", name="clip-img-item", - on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id), + on_clicked=partial(self._on_clip_clicked, item["id"]), ) button.set_style( f"background-image: url('{output_file}'); background-size: cover; background-position: center;" @@ -178,17 +184,15 @@ class CliphistManager: v_align="center", h_align="start", ), - on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id), + on_clicked=partial(self._on_clip_clicked, item["id"]), name="clip-item", ) def arrange_viewport(self, query: str = ""): if not self.launcher.viewport: return - self.launcher.viewport.children = [] filtered_clips = self.query_clips(query) - for clip in filtered_clips: button = self.bake_clip_slot(clip) if button: diff --git a/modules/launcher/components/dashboard.py b/modules/launcher/components/dashboard.py index da835aa..2dbe112 100644 --- a/modules/launcher/components/dashboard.py +++ b/modules/launcher/components/dashboard.py @@ -9,7 +9,7 @@ from gi.repository import Gtk, Gdk, GLib # Added Gdk import from fabric.utils import get_relative_path gi.require_version("Gtk", "3.0") -import snippets.iconss as icons +import utils.icons as icons def add_hover_cursor(widget): diff --git a/modules/launcher/components/emoji.py b/modules/launcher/components/emoji.py index 731c108..b3d688b 100644 --- a/modules/launcher/components/emoji.py +++ b/modules/launcher/components/emoji.py @@ -1,7 +1,7 @@ import subprocess from typing import Generator, List - import ijson +from functools import partial from fabric.utils import remove_handler, get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -18,26 +18,23 @@ class Emoji(Box): visible=False, **kwargs, ) - self.launcher = kwargs["launcher"] - self._arranger_handler: int = 0 + self._arranger_handler = 0 self.emoji_manager = EmojiManager(self) self.viewport = None self.search_entry = Entry( name="search-entry", h_expand=True, - notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()), - on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()), + notify_text=self._on_search_input, + on_activate=self._on_search_input, ) self.header_box = Box( name="header-box", spacing=10, orientation="h", - children=[ - self.search_entry, - ], + children=[self.search_entry], ) self.launcher_box = Box( @@ -49,6 +46,9 @@ class Emoji(Box): self.add(self.launcher_box) + def _on_search_input(self, entry, *args): + self.handle_search_input(entry.get_text()) + def open_launcher(self): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") @@ -63,12 +63,11 @@ class Emoji(Box): self.viewport.children = [] self.emoji_manager.arrange_viewport() - self.viewport.show() self.search_entry.grab_focus() def close_launcher(self): - self.launcher.close() + self.launcher.close_launcher() def handle_search_input(self, text: str): self.emoji_manager.arrange_viewport(text) @@ -105,7 +104,7 @@ class EmojiManager: name="emoji-item", child=Label(label=emoji[0], h_align="center"), tooltip_text=emoji[1], - on_clicked=lambda *_: self.copy_emoji(emoji), + on_clicked=partial(self.copy_emoji, emoji), **kwargs, ) @@ -113,11 +112,8 @@ class EmojiManager: if not self.launcher.viewport: return - ( + if self.launcher._arranger_handler: remove_handler(self.launcher._arranger_handler) - if self.launcher._arranger_handler - else None - ) self.launcher.viewport.children = [] filtered_emojis = self.query_emojis(query) diff --git a/modules/launcher/components/power.py b/modules/launcher/components/power.py index 6964a12..b441e25 100644 --- a/modules/launcher/components/power.py +++ b/modules/launcher/components/power.py @@ -2,7 +2,7 @@ from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.button import Button from fabric.utils.helpers import exec_shell_command_async -import snippets.iconss as icons +import utils.icons as icons class PowerMenu(Box): @@ -86,4 +86,3 @@ class PowerMenu(Box): def poweroff(self, *args): exec_shell_command_async("systemctl poweroff") self.close_menu() - diff --git a/modules/launcher/components/sh.py b/modules/launcher/components/sh.py index e9a92de..194b75b 100644 --- a/modules/launcher/components/sh.py +++ b/modules/launcher/components/sh.py @@ -9,7 +9,7 @@ from fabric.widgets.scrolledwindow import ScrolledWindow CACHE_DIR = os.getenv( - "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "fabric") + "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "modus") ) BINS = os.path.join(CACHE_DIR, "binaries") diff --git a/modules/launcher/components/todo.py b/modules/launcher/components/todo.py index ef81852..b34bc94 100644 --- a/modules/launcher/components/todo.py +++ b/modules/launcher/components/todo.py @@ -6,7 +6,7 @@ from fabric.widgets.button import Button from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow -import snippets.iconss as icons +import utils.icons as icons class TodoManager(Box): @@ -83,7 +83,7 @@ class TodoListManager: def __init__(self, launcher): self.launcher = launcher self.todos = [] - self.todo_file = os.path.expanduser("~/.local/share/modus/todos.json") + self.todo_file = os.path.expanduser("~/.local/state/modus/todos.json") self.load_todos() def load_todos(self): diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py index b777155..b3eb956 100644 --- a/modules/launcher/components/wallpapers.py +++ b/modules/launcher/components/wallpapers.py @@ -1,21 +1,21 @@ -import hashlib -import json -import os +import hashlib, json, os, colorsys from PIL import Image 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 -import concurrent.futures -from concurrent.futures import ThreadPoolExecutor -from snippets import WALLPAPERS_DIR +from fabric.utils import get_relative_path, exec_shell_command_async +from concurrent.futures import ThreadPoolExecutor, wait +from utils import WALLPAPERS_DIR +import utils.icons as icons 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", @@ -27,6 +27,7 @@ class WallpaperSelector(Box): "Fidelity": "fidelity", "Content": "content", } + ITEM_WIDTH = 108 def __init__(self, **kwargs): super().__init__( @@ -38,24 +39,32 @@ class WallpaperSelector(Box): **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) # Shared executor + 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() - self.selected_index = -1 - - # IconView for displaying thumbnails. + def _init_widgets(self): self.viewport = Gtk.IconView(name="wallpaper-icons") self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str)) self.viewport.set_pixbuf_column(0) self.viewport.set_text_column(-1) - self.viewport.set_item_width(0) + self.viewport.set_item_width(self.ITEM_WIDTH) self.viewport.connect("item-activated", self.on_wallpaper_selected) - self.scrolled_window = ScrolledWindow( name="scrolled-window", spacing=10, @@ -63,94 +72,117 @@ class WallpaperSelector(Box): v_expand=True, child=self.viewport, ) - self.search_entry = Entry( name="search-entry-walls", 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.connect("focus-out-event", self.on_search_entry_focus_out) - 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") self.scheme_dropdown.connect("changed", self.on_scheme_changed) - - self.dropdown_box = Box( - name="dropdown-box", - children=[ - self.scheme_dropdown, - ], + 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) + 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, ) - self.header_box = CenterBox( name="header-box", spacing=10, orientation="h", - start_children=[self.search_entry], + 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], ) - self.add(self.header_box) self.add(self.scrolled_window) - self._start_thumbnail_thread() - self.setup_file_monitor() - self.show_all() - self.search_entry.grab_focus() def setup_file_monitor(self): gfile = Gio.File.new_for_path(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) - 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._delete_cache(file_name) 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) and file_name not in self.files: + 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() - 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) + 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 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: + 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(), + ): model.append([pixbuf, file_name]) - - if query.strip() == "": + if not query.strip(): self.viewport.unselect_all() self.selected_index = -1 elif len(model) > 0: @@ -161,59 +193,37 @@ class WallpaperSelector(Box): file_name = model[path][1] full_path = os.path.join(WALLPAPERS_DIR, file_name) selected_scheme = self.scheme_dropdown.get_active_id() - wallpaper = get_relative_path("../../../config/scripts/wallpaper.py") - command = f"python -O {wallpaper} -I {full_path}" - GLib.spawn_command_line_async(command) - self.update_scheme(selected_scheme) - - 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 - ) - print(f"Color scheme selected: {display_name} ({scheme_id})") - self.update_scheme(scheme_id) - - color_generator = get_relative_path( - "../../../config/material-colors/generate.py" - ) - try: - command = f'python -O {color_generator} -R --scheme "{scheme_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) - print(f"Applied color scheme: {display_name}") - except Exception as e: - print(f"Failed to apply color scheme: {e}") - - def update_scheme(self, scheme: str): - try: - with open(self.SETTINGS_FILE, "r") as f: - settings = json.loads(f.read()) - - settings["generation-scheme"] = scheme - - with open(self.SETTINGS_FILE, "w") as f: - json.dump(settings, f, indent=2) - except Exception as error: - print(f"Failed to update generation-scheme in settings.json: {error}") + self.update_scheme(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) 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 + 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 + new_index = ( + (current_index - 1) % len(scheme_list) + if event.keyval == Gdk.KEY_Up + else (current_index + 1) % len(scheme_list) ) - if event.keyval == Gdk.KEY_Up: - new_index = (current_index - 1) % len(schemes_list) - else: - new_index = (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 @@ -222,7 +232,6 @@ class WallpaperSelector(Box): path = Gtk.TreePath.new_from_indices([self.selected_index]) self.on_wallpaper_selected(self.viewport, path) return True - return False def move_selection_2d(self, keyval): @@ -230,33 +239,24 @@ 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 ) else: current_index = self.selected_index - # Calculate columns based on the allocation width. allocation = self.viewport.get_allocation() - item_width = 108 - 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 - - # Ensure new_index remains within limits. - if new_index < 0: - new_index = 0 - if new_index >= total_items: - new_index = total_items - 1 - + 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)) self.update_selection(new_index) def update_selection(self, new_index: int): @@ -267,14 +267,14 @@ class WallpaperSelector(Box): self.selected_index = new_index def _start_thumbnail_thread(self): - thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None) + 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) + wait(futures) GLib.idle_add(self._process_batch) def _process_file(self, file_name): @@ -287,13 +287,10 @@ class WallpaperSelector(Box): side = min(width, height) left = (width - side) // 2 top = (height - side) // 2 - right = left + side - bottom = top + side - img_cropped = img.crop((left, top, right, bottom)) + img_cropped = img.crop((left, top, left + side, top + side)) 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}") + except Exception: return self.thumbnail_queue.append((cache_path, file_name)) GLib.idle_add(self._process_batch) @@ -316,13 +313,110 @@ 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") ) - def on_search_entry_focus_out(self, widget, event): - if self.get_mapped(): - widget.grab_focus() - return False + @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) diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py index b2f656e..657d56c 100644 --- a/modules/launcher/components/wifi.py +++ b/modules/launcher/components/wifi.py @@ -5,7 +5,7 @@ from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.scrolledwindow import ScrolledWindow from services import NetworkClient -import snippets.iconss as icons +import utils.icons as icons class WifiNetworkSlot(CenterBox): diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py index 97a950e..ca99e66 100644 --- a/modules/launcher/launcher.py +++ b/modules/launcher/launcher.py @@ -38,13 +38,12 @@ class Launcher(Window): self.wifi = WifiManager() self.calendar = Calendar() + # Wrap the dashboard in a Box container self.dashboard = Box( name="dashboard", orientation="h", spacing=10, - children=[ - self.dashboard, - ], + children=[self.dashboard], ) self.launcher = AppLauncher(launcher=self) @@ -78,7 +77,10 @@ class Launcher(Window): self.add(self.launcher_box) self.show_all() self.hide() - self.add_keybinding("Escape", lambda *_: self.close()) + self.add_keybinding("Escape", self._on_escape) + + def _on_escape(self, *args): + return self.close() def close(self): self.set_keyboard_mode("none")