diff --git a/modules/launcher/plugins/applications.py b/modules/launcher/plugins/applications.py index 9b1e297..0d05d70 100644 --- a/modules/launcher/plugins/applications.py +++ b/modules/launcher/plugins/applications.py @@ -18,7 +18,7 @@ class ApplicationsPlugin(PluginBase): def initialize(self): # Set up triggers for applications - both with and without spaces - self.set_triggers(["app", "app "]) + self.set_triggers(["app"]) self._load_applications() def cleanup(self): diff --git a/modules/launcher/plugins/archive/files.py b/modules/launcher/plugins/archive/files.py index a118f30..b654969 100644 --- a/modules/launcher/plugins/archive/files.py +++ b/modules/launcher/plugins/archive/files.py @@ -1,19 +1,15 @@ -""" -Files plugin for the launcher. -High-performance file search (files only, excludes directories) using advanced fuzzy matching. -""" - import os import subprocess import threading import time -from typing import List, Dict, Set, Tuple -from pathlib import Path -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import ThreadPoolExecutor +from typing import Dict, List + from thefuzz import fuzz + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -import utils.icons as icons class FilesPlugin(PluginBase): @@ -69,8 +65,6 @@ class FilesPlugin(PluginBase): [ "file", "find", - "file ", - "find ", ] ) self._warm_cache() @@ -153,7 +147,10 @@ class FilesPlugin(PluginBase): continue # Skip home directory if we already have enough results from specific folders - if search_path == os.path.expanduser("~") and len(results) >= self.max_results: + if ( + search_path == os.path.expanduser("~") + and len(results) >= self.max_results + ): continue cached_files = self._get_cached_files(search_path) @@ -217,9 +214,19 @@ class FilesPlugin(PluginBase): # Skip common large directories that are unlikely to contain user files if entry.is_dir() and entry.name in { - "node_modules", ".git", ".cache", "__pycache__", - ".npm", ".cargo", ".rustup", "target", "build", - ".vscode", ".idea", "venv", ".env" + "node_modules", + ".git", + ".cache", + "__pycache__", + ".npm", + ".cargo", + ".rustup", + "target", + "build", + ".vscode", + ".idea", + "venv", + ".env", }: continue @@ -361,7 +368,8 @@ class FilesPlugin(PluginBase): if len(query_lower) >= 3: # Use only the most effective fuzzy matching methods return ( - fuzz.partial_ratio(query_lower, text_lower) >= self.fuzzy_thresholds["partial"] + fuzz.partial_ratio(query_lower, text_lower) + >= self.fuzzy_thresholds["partial"] or fuzz.ratio(query_lower, text_lower) >= self.fuzzy_thresholds["ratio"] ) diff --git a/modules/launcher/plugins/bluetooth.py b/modules/launcher/plugins/bluetooth.py index 2d98d94..129f1c3 100644 --- a/modules/launcher/plugins/bluetooth.py +++ b/modules/launcher/plugins/bluetooth.py @@ -1,13 +1,9 @@ -""" -Bluetooth plugin for the launcher. -Manage Bluetooth devices, connections, and adapter settings. -""" - from typing import List + +import utils.icons as icons from fabric.bluetooth import BluetoothClient, BluetoothDevice from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -import utils.icons as icons class BluetoothPlugin(PluginBase): @@ -41,6 +37,7 @@ class BluetoothPlugin(PluginBase): if self._scan_timeout_id: try: from gi.repository import GLib + GLib.source_remove(self._scan_timeout_id) self._scan_timeout_id = None except Exception: @@ -114,9 +111,15 @@ class BluetoothPlugin(PluginBase): battery_info = "" if device.battery_percentage > 0: battery_info = f" ({device.battery_percentage:.0f}%)" - return f"Connected to {device.alias}", f"Device: {device.type}{battery_info}" + return ( + f"Connected to {device.alias}", + f"Device: {device.type}{battery_info}", + ) else: - return f"Connected to {len(connected_devices)} devices", "Multiple devices connected" + return ( + f"Connected to {len(connected_devices)} devices", + "Multiple devices connected", + ) elif self.bluetooth_client.scanning: return "Scanning for devices...", "Looking for nearby Bluetooth devices" else: @@ -126,7 +129,11 @@ class BluetoothPlugin(PluginBase): """Get appropriate icon for a bluetooth device based on its type.""" device_type = device.type.lower() - if "audio" in device_type or "headphone" in device_type or "headset" in device_type: + if ( + "audio" in device_type + or "headphone" in device_type + or "headset" in device_type + ): return icons.headphones elif "mouse" in device_type or "keyboard" in device_type: return icons.keyboard @@ -153,6 +160,7 @@ class BluetoothPlugin(PluginBase): if self._scan_timeout_id: try: from gi.repository import GLib + GLib.source_remove(self._scan_timeout_id) self._scan_timeout_id = None except Exception: @@ -186,6 +194,7 @@ class BluetoothPlugin(PluginBase): if self._scan_timeout_id: try: from gi.repository import GLib + GLib.source_remove(self._scan_timeout_id) self._scan_timeout_id = None except Exception: @@ -200,6 +209,7 @@ class BluetoothPlugin(PluginBase): # Stop scanning if plugin hasn't been used for 10 seconds import time + current_time = time.time() if current_time - self._last_query_time > 10: self._stop_scanning() @@ -222,14 +232,18 @@ class BluetoothPlugin(PluginBase): def _create_connect_action(self, device: BluetoothDevice): """Create a connect action function with proper closure.""" + def connect_action(): return self._connect_device(device) + return connect_action def _create_disconnect_action(self, device: BluetoothDevice): """Create a disconnect action function with proper closure.""" + def disconnect_action(): return self._disconnect_device(device) + return disconnect_action def _force_launcher_refresh(self): @@ -242,13 +256,16 @@ class BluetoothPlugin(PluginBase): try: # Try to access the launcher through the fabric Application from fabric import Application + app = Application.get_default() - if app and hasattr(app, 'launcher'): + if app and hasattr(app, "launcher"): launcher = app.launcher - if launcher and hasattr(launcher, '_perform_search'): + if launcher and hasattr(launcher, "_perform_search"): # Get current query and trigger a search - current_query = launcher.query if hasattr(launcher, 'query') else "" + current_query = ( + launcher.query if hasattr(launcher, "query") else "" + ) if not current_query: # If no query, use the trigger to force refresh current_query = "bt list" @@ -259,9 +276,15 @@ class BluetoothPlugin(PluginBase): # Fallback: try to find launcher instance through other means import gc + for obj in gc.get_objects(): - if hasattr(obj, '__class__') and obj.__class__.__name__ == 'Launcher': - if hasattr(obj, '_perform_search') and hasattr(obj, 'query'): + if ( + hasattr(obj, "__class__") + and obj.__class__.__name__ == "Launcher" + ): + if hasattr(obj, "_perform_search") and hasattr( + obj, "query" + ): current_query = obj.query if obj.query else "bt list" obj._perform_search(current_query) return False @@ -281,12 +304,16 @@ class BluetoothPlugin(PluginBase): """Process bluetooth queries.""" # Update last query time to track plugin activity import time + self._last_query_time = time.time() # Schedule periodic check for inactive scanning try: from gi.repository import GLib - GLib.timeout_add_seconds(15, lambda: (self._check_and_stop_inactive_scanning(), False)) + + GLib.timeout_add_seconds( + 15, lambda: (self._check_and_stop_inactive_scanning(), False) + ) except Exception: pass @@ -340,7 +367,6 @@ class BluetoothPlugin(PluginBase): ) ) - else: results.append( Result( @@ -411,7 +437,10 @@ class BluetoothPlugin(PluginBase): relevance = 1.0 elif device.connecting: subtitle = f"Connecting... • {device.type}" - action = lambda: None + + def action(): + return None + relevance = 0.9 else: subtitle = f"Disconnected • {device.type} • Click to connect" @@ -431,7 +460,7 @@ class BluetoothPlugin(PluginBase): "device_address": device.address, "device_name": device.alias, "connected": device.connected, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) @@ -470,14 +499,14 @@ class BluetoothPlugin(PluginBase): return results - - # Search for devices by name (like network plugin search) if self.bluetooth_client and self.bluetooth_client.enabled: devices = self.bluetooth_client.devices matching_devices = [ - device for device in devices - if query_lower in device.alias.lower() or query_lower in device.name.lower() + device + for device in devices + if query_lower in device.alias.lower() + or query_lower in device.name.lower() ] if matching_devices: @@ -501,7 +530,10 @@ class BluetoothPlugin(PluginBase): relevance = 1.0 elif device.connecting: subtitle = f"Connecting... • {device.type}" - action = lambda: None + + def action(): + return None + relevance = 0.9 else: subtitle = f"Disconnected • {device.type} • Click to connect" @@ -521,7 +553,7 @@ class BluetoothPlugin(PluginBase): "device_address": device.address, "device_name": device.alias, "connected": device.connected, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) @@ -549,11 +581,13 @@ class BluetoothPlugin(PluginBase): title="Bluetooth Not Available", subtitle="Enable Bluetooth to search for devices", icon_markup=icons.bluetooth_off, - action=lambda: self._toggle_bluetooth() if self.bluetooth_client else None, + action=lambda: self._toggle_bluetooth() + if self.bluetooth_client + else None, relevance=0.5, plugin_name=self.display_name, data={"type": "info"}, ) ) - return results \ No newline at end of file + return results diff --git a/modules/launcher/plugins/caffeine.py b/modules/launcher/plugins/caffeine.py index f57ee8b..ec1cc5a 100644 --- a/modules/launcher/plugins/caffeine.py +++ b/modules/launcher/plugins/caffeine.py @@ -1,19 +1,14 @@ -""" -Caffeine plugin for the launcher. -Prevents system from going idle for a specified duration. -""" - import subprocess -from typing import List from threading import Timer +from typing import List +import config.data as data import gi import utils.icons as icons from fabric.utils import get_relative_path from fabric.utils.helpers import exec_shell_command_async from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -import config.data as data gi.require_version("Gtk", "3.0") @@ -42,7 +37,7 @@ class CaffeinePlugin(PluginBase): def initialize(self): """Initialize the caffeine plugin.""" - self.set_triggers(["caffeine", "caffeine "]) + self.set_triggers(["caffeine"]) def cleanup(self): """Cleanup the caffeine plugin.""" @@ -65,13 +60,17 @@ class CaffeinePlugin(PluginBase): if duration.lower() == "off": # Deactivation notification exec_shell_command_async( - f"notify-send '☕ Caffeine' 'Deactivated' -a '{data.APP_NAME_CAP}' -e" + f"notify-send '☕ Caffeine' 'Deactivated' -a '{ + data.APP_NAME_CAP + }' -e" ) else: # Activation notification with duration duration_text = self.durations.get(duration, duration) exec_shell_command_async( - f"notify-send '☕ Caffeine' 'Activated for {duration_text}' -a '{data.APP_NAME_CAP}' -e" + f"notify-send '☕ Caffeine' 'Activated for { + duration_text + }' -a '{data.APP_NAME_CAP}' -e" ) # Schedule expiration notification for timed durations @@ -86,13 +85,13 @@ class CaffeinePlugin(PluginBase): def _parse_duration_to_seconds(self, duration_str: str) -> int: """Parse duration string into seconds. Same logic as inhibit.py""" try: - if duration_str.lower() in ['on', 'off']: + if duration_str.lower() in ["on", "off"]: return 0 - elif duration_str.endswith('h'): + elif duration_str.endswith("h"): return int(float(duration_str[:-1]) * 3600) - elif duration_str.endswith('m'): + elif duration_str.endswith("m"): return int(float(duration_str[:-1]) * 60) - elif duration_str.endswith('s'): + elif duration_str.endswith("s"): return int(float(duration_str[:-1])) else: return int(duration_str) @@ -103,9 +102,12 @@ class CaffeinePlugin(PluginBase): """Schedule a notification for when the caffeine effect expires.""" seconds = self._parse_duration_to_seconds(duration) if seconds > 0: + def send_expiration_notification(): exec_shell_command_async( - f"notify-send '☕ Caffeine' 'Expired after {duration_text}' -a '{data.APP_NAME_CAP}' -e" + f"notify-send '☕ Caffeine' 'Expired after {duration_text}' -a '{ + data.APP_NAME_CAP + }' -e" ) # Schedule the notification diff --git a/modules/launcher/plugins/calculator.py b/modules/launcher/plugins/calculator.py index cbae294..d0dcee5 100644 --- a/modules/launcher/plugins/calculator.py +++ b/modules/launcher/plugins/calculator.py @@ -1,8 +1,3 @@ -""" -Calculator plugin for the launcher. -Evaluates mathematical expressions and converts units. -""" - import math import re import subprocess @@ -64,7 +59,7 @@ class CalculatorPlugin(PluginBase): def initialize(self): """Initialize the files plugin.""" - self.set_triggers(["=", "= "]) + self.set_triggers(["="]) def _cleanup_cache(self): """Clean up old cache entries.""" diff --git a/modules/launcher/plugins/calendar.py b/modules/launcher/plugins/calendar.py index 8d35f26..1bfd27f 100644 --- a/modules/launcher/plugins/calendar.py +++ b/modules/launcher/plugins/calendar.py @@ -1,8 +1,3 @@ -""" -Calendar plugin for the launcher. -Calendar with visual dates and working Enter key navigation. -""" - import calendar import datetime from typing import List @@ -11,7 +6,6 @@ import gi import utils.icons as icons from fabric.widgets.label import Label from gi.repository import Gtk -from fabric.widgets.button import Button from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -22,7 +16,9 @@ class CalendarWidget(Gtk.Box): """Visual calendar widget showing month grid with dates and navigation buttons.""" def __init__(self, month: int, year: int, plugin_instance=None): - super().__init__(name="calendar-widget", orientation=Gtk.Orientation.VERTICAL, spacing=2) + super().__init__( + name="calendar-widget", orientation=Gtk.Orientation.VERTICAL, spacing=2 + ) # Set size constraints to fit within launcher (following CSS) self.set_size_request(540, 240) @@ -48,7 +44,9 @@ class CalendarWidget(Gtk.Box): header_box.set_name("calendar-header-box") # Previous button - prev_button = Gtk.Button(child=Label(markup=icons.chevron_left, name="calendar-nav-button")) + prev_button = Gtk.Button( + child=Label(markup=icons.chevron_left, name="calendar-nav-button") + ) prev_button.connect("clicked", self._on_previous_clicked) # Month/Year label (following CSS naming) @@ -56,12 +54,15 @@ class CalendarWidget(Gtk.Box): self.month_label = Label( name="calendar-month-year", label=f"{month_name} {self.display_year}", - h_align="center" + h_align="center", ) - self.month_label.set_size_request(150, -1) # Fixed width for consistent layout + # Fixed width for consistent layout + self.month_label.set_size_request(150, -1) # Next button - next_button = Gtk.Button(child = Label(markup=icons.chevron_right, name="calendar-nav-button")) + next_button = Gtk.Button( + child=Label(markup=icons.chevron_right, name="calendar-nav-button") + ) next_button.connect("clicked", self._on_next_clicked) # Add to header box @@ -75,16 +76,14 @@ class CalendarWidget(Gtk.Box): column_spacing=1, row_spacing=1, column_homogeneous=True, - row_homogeneous=True + row_homogeneous=True, ) # Day headers (following CSS naming) - day_names = ['Sun','Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] for i, day_name in enumerate(day_names): day_header = Label( - name="calendar-day-header", - label=day_name, - h_align="center" + name="calendar-day-header", label=day_name, h_align="center" ) grid.attach(day_header, i, 0, 1, 1) @@ -99,15 +98,15 @@ class CalendarWidget(Gtk.Box): else: # Day label (following CSS naming) day_label = Label( - name="calendar-day-label", - label=str(day), - h_align="center" + name="calendar-day-label", label=str(day), h_align="center" ) # Highlight today (CSS class) - if (day == self.today.day and - self.display_month == self.today.month and - self.display_year == self.today.year): + if ( + day == self.today.day + and self.display_month == self.today.month + and self.display_year == self.today.year + ): day_label.get_style_context().add_class("today") # Highlight weekends (CSS class) @@ -181,16 +180,14 @@ class CalendarWidget(Gtk.Box): column_spacing=1, row_spacing=1, column_homogeneous=True, - row_homogeneous=True + row_homogeneous=True, ) # Day headers (following CSS naming) - day_names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] for i, day_name in enumerate(day_names): day_header = Label( - name="calendar-day-header", - label=day_name, - h_align="center" + name="calendar-day-header", label=day_name, h_align="center" ) grid.attach(day_header, i, 0, 1, 1) @@ -205,15 +202,15 @@ class CalendarWidget(Gtk.Box): else: # Day label (following CSS naming) day_label = Label( - name="calendar-day-label", - label=str(day), - h_align="center" + name="calendar-day-label", label=str(day), h_align="center" ) # Highlight today (CSS class) - if (day == self.today.day and - month == self.today.month and - year == self.today.year): + if ( + day == self.today.day + and month == self.today.month + and year == self.today.year + ): day_label.get_style_context().add_class("today") # Highlight weekends (CSS class) @@ -244,7 +241,7 @@ class CalendarPlugin(PluginBase): def initialize(self): """Initialize the Calendar plugin.""" - self.set_triggers(["cal", "cal "]) + self.set_triggers(["cal"]) self.description = "Calendar with visual dates and navigation. Use ← → arrows for months, Home for current month" def cleanup(self): @@ -273,7 +270,7 @@ class CalendarPlugin(PluginBase): icon_markup=icons.calendar, action=lambda: self._copy_to_clipboard(date_str), relevance=1.0, - plugin_name=self.display_name + plugin_name=self.display_name, ) ) return results @@ -288,7 +285,7 @@ class CalendarPlugin(PluginBase): icon_markup=icons.calendar, action=lambda: self._copy_to_clipboard(date_str), relevance=1.0, - plugin_name=self.display_name + plugin_name=self.display_name, ) ) return results @@ -303,7 +300,7 @@ class CalendarPlugin(PluginBase): icon_markup=icons.calendar, action=lambda: self._copy_to_clipboard(date_str), relevance=1.0, - plugin_name=self.display_name + plugin_name=self.display_name, ) ) return results @@ -344,7 +341,7 @@ class CalendarPlugin(PluginBase): relevance=1.0, plugin_name=self.display_name, custom_widget=calendar_widget, - data={"type": "calendar_display"} + data={"type": "calendar_display"}, ) ) @@ -353,7 +350,9 @@ class CalendarPlugin(PluginBase): def _navigate_to_next(self): """Navigate to next month (called by button click).""" if self._current_month and self._current_year: - next_month, next_year = self._advance_month(self._current_month, self._current_year, 1) + next_month, next_year = self._advance_month( + self._current_month, self._current_year, 1 + ) self._current_month = next_month self._current_year = next_year @@ -364,7 +363,9 @@ class CalendarPlugin(PluginBase): def _navigate_to_previous(self): """Navigate to previous month (called by button click).""" if self._current_month and self._current_year: - prev_month, prev_year = self._advance_month(self._current_month, self._current_year, -1) + prev_month, prev_year = self._advance_month( + self._current_month, self._current_year, -1 + ) self._current_month = prev_month self._current_year = prev_year @@ -395,6 +396,7 @@ class CalendarPlugin(PluginBase): """Copy text to clipboard.""" try: import subprocess + subprocess.run(["wl-copy"], input=text.encode(), check=True) print(f"Copied to clipboard: {text}") except subprocess.CalledProcessError: diff --git a/modules/launcher/plugins/clipboard.py b/modules/launcher/plugins/clipboard.py index 36fed0a..8182c79 100644 --- a/modules/launcher/plugins/clipboard.py +++ b/modules/launcher/plugins/clipboard.py @@ -1,19 +1,15 @@ -""" -Clipboard plugin using cliphist for clipboard history management. -Optimized for performance with caching, threading, and lazy loading. -""" - import os import subprocess import sys import tempfile import threading import time -from typing import List, Dict, Optional from concurrent.futures import ThreadPoolExecutor +from typing import Dict, List, Optional + +from gi.repository import GdkPixbuf, GLib from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -from gi.repository import GdkPixbuf, GLib class ClipboardPlugin(PluginBase): @@ -25,7 +21,8 @@ class ClipboardPlugin(PluginBase): # Performance settings self.max_results = 20 - self.cache_ttl = 5 # Cache clipboard items for 5 seconds (more responsive) + # Cache clipboard items for 5 seconds (more responsive) + self.cache_ttl = 5 self.image_cache_ttl = 300 # Cache images for 5 minutes # Initialize cache and temp directory @@ -36,7 +33,9 @@ class ClipboardPlugin(PluginBase): self.image_cache_timestamps: Dict[str, float] = {} # Threading - self.executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="clipboard") + self.executor = ThreadPoolExecutor( + max_workers=2, thread_name_prefix="clipboard" + ) self.cache_lock = threading.Lock() # State tracking @@ -45,9 +44,11 @@ class ClipboardPlugin(PluginBase): def initialize(self): """Initialize the plugin.""" - self.set_triggers(["clip", "clip "]) + self.set_triggers(["clip"]) try: - subprocess.run(["cliphist", "list"], capture_output=True, check=True, timeout=5) + subprocess.run( + ["cliphist", "list"], capture_output=True, check=True, timeout=5 + ) except (subprocess.SubprocessError, FileNotFoundError): raise RuntimeError("cliphist is not installed or not working properly") @@ -58,12 +59,13 @@ class ClipboardPlugin(PluginBase): """Cleanup the plugin.""" try: # Shutdown executor - if hasattr(self, 'executor'): + if hasattr(self, "executor"): self.executor.shutdown(wait=False) # Clean up temp files if os.path.exists(self.tmp_dir): import shutil + shutil.rmtree(self.tmp_dir) # Clear caches @@ -91,11 +93,16 @@ class ClipboardPlugin(PluginBase): try: # Try to access the launcher through the fabric Application from fabric import Application + app = Application.get_default() - if app and hasattr(app, 'launcher'): + if app and hasattr(app, "launcher"): launcher = app.launcher - if launcher and hasattr(launcher, 'search_entry') and hasattr(launcher, '_perform_search'): + if ( + launcher + and hasattr(launcher, "search_entry") + and hasattr(launcher, "_perform_search") + ): # Get current search text to preserve the query current_text = launcher.search_entry.get_text() # Trigger the search to refresh results @@ -104,9 +111,15 @@ class ClipboardPlugin(PluginBase): # Fallback: try to find launcher instance through other means import gc + for obj in gc.get_objects(): - if (hasattr(obj, '__class__') and obj.__class__.__name__ == 'Launcher'): - if hasattr(obj, 'search_entry') and hasattr(obj, '_perform_search'): + if ( + hasattr(obj, "__class__") + and obj.__class__.__name__ == "Launcher" + ): + if hasattr(obj, "search_entry") and hasattr( + obj, "_perform_search" + ): current_text = obj.search_entry.get_text() obj._perform_search(current_text) return False @@ -122,8 +135,6 @@ class ClipboardPlugin(PluginBase): except Exception as e: print(f"Could not trigger refresh: {e}") - - def _load_clipboard_items_cached(self) -> List[str]: """Load clipboard items from cliphist with caching and change detection.""" current_time = time.time() @@ -136,22 +147,26 @@ class ClipboardPlugin(PluginBase): stdout_str = result.stdout.decode("utf-8", errors="replace") if stdout_str.strip(): lines = stdout_str.strip().split("\n") - items = [line for line in lines if line and " List[Result]: """Process network queries.""" results = [] @@ -506,7 +525,9 @@ class NetworkPlugin(PluginBase): results.append( Result( title="Disconnect WiFi", - subtitle=f"Disconnect from {self.network_client.wifi_device.active_access_point.ssid}", + subtitle=f"Disconnect from { + self.network_client.wifi_device.active_access_point.ssid + }", icon_markup=icons.world_off, action=lambda: self._disconnect_wifi(), relevance=0.8, @@ -548,8 +569,11 @@ class NetworkPlugin(PluginBase): ) # Show current networks if any - if (self._ready and self.network_client.wifi_device and - self.network_client.wifi_device.wireless_enabled): + if ( + self._ready + and self.network_client.wifi_device + and self.network_client.wifi_device.wireless_enabled + ): access_points = self.network_client.wifi_device.access_points if access_points: results.append( @@ -567,8 +591,10 @@ class NetworkPlugin(PluginBase): return results if query_lower in ["disconnect", "off"]: - if (self.network_client.wifi_device and - self.network_client.wifi_device.active_access_point): + if ( + self.network_client.wifi_device + and self.network_client.wifi_device.active_access_point + ): ap = self.network_client.wifi_device.active_access_point results.append( Result( @@ -618,9 +644,11 @@ class NetworkPlugin(PluginBase): return results # Search for specific networks or show available networks - if (self._ready and self.network_client.wifi_device and - self.network_client.wifi_device.wireless_enabled): - + if ( + self._ready + and self.network_client.wifi_device + and self.network_client.wifi_device.wireless_enabled + ): access_points = self.network_client.wifi_device.access_points if query_lower in ["list"]: @@ -649,11 +677,13 @@ class NetworkPlugin(PluginBase): title = ap.ssid # Check if this network should show password entry instead - if ap.requires_password and self.showing_password_for == ap.ssid: + if ( + ap.requires_password + and self.showing_password_for == ap.ssid + ): # REPLACE network with password entry Result using our own Entry widget password_widget = NetworkPasswordEntry( - access_point=ap, - network_plugin=self + access_point=ap, network_plugin=self ) results.append( @@ -668,7 +698,7 @@ class NetworkPlugin(PluginBase): data={ "type": "password_entry", "ssid": ap.ssid, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) @@ -676,14 +706,26 @@ class NetworkPlugin(PluginBase): # Show normal network Result if ap.is_active: subtitle = f"Connected • {ap.strength}% signal" - action = lambda: self._disconnect_wifi() + + def action(): + return self._disconnect_wifi() else: if ap.requires_password: - subtitle = f"{ap.strength}% signal • Click to enter password" - action = lambda current_ap=ap: self._show_password_entry_for_network(current_ap) + subtitle = f"{ + ap.strength + }% signal • Click to enter password" + + def action(current_ap=ap): + return self._show_password_entry_for_network( + current_ap + ) else: - subtitle = f"{ap.strength}% signal • Click to connect" - action = lambda current_ap=ap: self._connect_to_network(current_ap) + subtitle = ( + f"{ap.strength}% signal • Click to connect" + ) + + def action(current_ap=ap): + return self._connect_to_network(current_ap) results.append( Result( @@ -697,20 +739,17 @@ class NetworkPlugin(PluginBase): "type": "network", "ssid": ap.ssid, "requires_password": ap.requires_password, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) - - return results else: # Search for networks matching the query matching_networks = [ - ap for ap in access_points - if query.lower() in ap.ssid.lower() + ap for ap in access_points if query.lower() in ap.ssid.lower() ] if matching_networks: @@ -721,11 +760,13 @@ class NetworkPlugin(PluginBase): title = ap.ssid # Check if this network should show password entry instead - if ap.requires_password and self.showing_password_for == ap.ssid: + if ( + ap.requires_password + and self.showing_password_for == ap.ssid + ): # REPLACE network with password entry Result using our own Entry widget password_widget = NetworkPasswordEntry( - access_point=ap, - network_plugin=self + access_point=ap, network_plugin=self ) results.append( @@ -740,7 +781,7 @@ class NetworkPlugin(PluginBase): data={ "type": "password_entry", "ssid": ap.ssid, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) @@ -748,14 +789,26 @@ class NetworkPlugin(PluginBase): # Show normal network Result if ap.is_active: subtitle = f"Connected • {ap.strength}% signal" - action = lambda: self._disconnect_wifi() + + def action(): + return self._disconnect_wifi() else: if ap.requires_password: - subtitle = f"{ap.strength}% signal • Click to enter password" - action = lambda current_ap=ap: self._show_password_entry_for_network(current_ap) + subtitle = f"{ + ap.strength + }% signal • Click to enter password" + + def action(current_ap=ap): + return self._show_password_entry_for_network( + current_ap + ) else: - subtitle = f"{ap.strength}% signal • Click to connect" - action = lambda current_ap=ap: self._connect_to_network(current_ap) + subtitle = ( + f"{ap.strength}% signal • Click to connect" + ) + + def action(current_ap=ap): + return self._connect_to_network(current_ap) results.append( Result( @@ -769,13 +822,11 @@ class NetworkPlugin(PluginBase): "type": "network", "ssid": ap.ssid, "requires_password": ap.requires_password, - "keep_launcher_open": True + "keep_launcher_open": True, }, ) ) - - return results else: # No matching networks found @@ -798,11 +849,13 @@ class NetworkPlugin(PluginBase): title="WiFi Not Available", subtitle="Enable WiFi to search for networks", icon_markup=icons.wifi_off, - action=lambda: self._toggle_wifi() if self.network_client.wifi_device else None, + action=lambda: self._toggle_wifi() + if self.network_client.wifi_device + else None, relevance=0.5, plugin_name=self.display_name, data={"type": "info"}, ) ) - return results \ No newline at end of file + return results diff --git a/modules/launcher/plugins/notifications.py b/modules/launcher/plugins/notifications.py index 12606eb..d8753bc 100644 --- a/modules/launcher/plugins/notifications.py +++ b/modules/launcher/plugins/notifications.py @@ -1,19 +1,15 @@ -""" -Notification History plugin for the launcher. -Provides access to notification history with search, view, and management capabilities. -""" - import time from datetime import datetime from typing import List, Optional + +import utils.icons as icons +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.label import Label +from gi.repository import GdkPixbuf from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result from services import notification_service -from gi.repository import GdkPixbuf, GLib -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -import utils.icons as icons class NotificationDetailWidget(Box): @@ -42,7 +38,7 @@ class NotificationDetailWidget(Box): label=f"📧 {self.notification_data.get('app_name', 'Unknown')}", name="notification-detail-title", h_align="start", - h_expand=True + h_expand=True, ) header_box.add(title_label) @@ -50,7 +46,7 @@ class NotificationDetailWidget(Box): close_btn = Button( child=Label(markup=icons.cancel, name="notification-close-icon"), name="notification-close-btn", - on_clicked=lambda *_: self.plugin._close_detail_view() + on_clicked=lambda *_: self.plugin._close_detail_view(), ) header_box.add(close_btn) @@ -60,24 +56,24 @@ class NotificationDetailWidget(Box): content_box = Box(orientation="v", spacing=4) # Summary - summary = self.notification_data.get('summary', 'No title') + summary = self.notification_data.get("summary", "No title") summary_label = Label( label=f"Subject: {summary}", name="notification-detail-summary", h_align="start", - ellipsize="end" + ellipsize="end", ) content_box.add(summary_label) # Body content - body = self.notification_data.get('body', '') + body = self.notification_data.get("body", "") if body: body_label = Label( label=body, name="notification-detail-body", h_align="start", v_align="start", - ellipsize="none" + ellipsize="none", ) # Set line wrapping using properties body_label.set_property("wrap", True) @@ -85,12 +81,10 @@ class NotificationDetailWidget(Box): content_box.add(body_label) # Timestamp - timestamp = self.notification_data.get('timestamp', time.time()) + timestamp = self.notification_data.get("timestamp", time.time()) time_str = self.plugin._format_timestamp(timestamp) time_label = Label( - label=f"Time: {time_str}", - name="notification-detail-time", - h_align="start" + label=f"Time: {time_str}", name="notification-detail-time", h_align="start" ) content_box.add(time_label) @@ -101,9 +95,11 @@ class NotificationDetailWidget(Box): # Remove button remove_btn = Button( - child=Label(markup=f"{icons.trash} Remove", name="notification-action-label"), + child=Label( + markup=f"{icons.trash} Remove", name="notification-action-label" + ), name="notification-action-btn", - on_clicked=lambda *_: self._remove_notification() + on_clicked=lambda *_: self._remove_notification(), ) actions_box.add(remove_btn) @@ -111,7 +107,7 @@ class NotificationDetailWidget(Box): def _remove_notification(self): """Remove this notification.""" - notif_id = self.notification_data.get('notification_id', 0) + notif_id = self.notification_data.get("notification_id", 0) self.plugin._clear_notification(notif_id) @@ -129,16 +125,20 @@ class NotificationsPlugin(PluginBase): def initialize(self): """Initialize the plugin.""" - self.set_triggers(["notif", "notif "]) + self.set_triggers(["notif"]) # Connect to notification service signals to refresh when notifications change - self.notification_service.connect("notification_count", self._on_notification_count_changed) + self.notification_service.connect( + "notification_count", self._on_notification_count_changed + ) def cleanup(self): """Cleanup the plugin.""" # Disconnect from notification service signals try: - self.notification_service.disconnect("notification_count", self._on_notification_count_changed) + self.notification_service.disconnect( + "notification_count", self._on_notification_count_changed + ) except: pass @@ -166,18 +166,20 @@ class NotificationsPlugin(PluginBase): """Get icon for notification.""" try: # Try to use notification's image first - if hasattr(notification, 'image_pixbuf') and notification.image_pixbuf: + if hasattr(notification, "image_pixbuf") and notification.image_pixbuf: return notification.image_pixbuf.scale_simple( 32, 32, GdkPixbuf.InterpType.BILINEAR ) # Try app icon - if hasattr(notification, 'app_icon') and notification.app_icon: + if hasattr(notification, "app_icon") and notification.app_icon: if notification.app_icon.startswith("file://"): icon_path = notification.app_icon[7:] try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) - return pixbuf.scale_simple(32, 32, GdkPixbuf.InterpType.BILINEAR) + return pixbuf.scale_simple( + 32, 32, GdkPixbuf.InterpType.BILINEAR + ) except: pass @@ -191,7 +193,7 @@ class NotificationsPlugin(PluginBase): return "" if len(text) <= max_length: return text - return text[:max_length-3] + "..." + return text[: max_length - 3] + "..." def _trigger_refresh(self): """Trigger launcher refresh to return to default notification view.""" @@ -223,7 +225,9 @@ class NotificationsPlugin(PluginBase): hasattr(obj, "__class__") and obj.__class__.__name__ == "Launcher" ): - if hasattr(obj, "search_entry") and hasattr(obj, "_perform_search"): + if hasattr(obj, "search_entry") and hasattr( + obj, "_perform_search" + ): current_text = obj.search_entry.get_text() obj._perform_search(current_text) return False @@ -255,18 +259,34 @@ class NotificationsPlugin(PluginBase): count_after = len(self.notification_service.all_notifications) # Debug: Print removal result - print(f"[NotificationPlugin] Notifications before: {count_before}, after: {count_after}") + print( + f"[NotificationPlugin] Notifications before: {count_before}, after: { + count_after + }" + ) if count_after < count_before: - print(f"[NotificationPlugin] Successfully removed notification {notification_id}") + print( + f"[NotificationPlugin] Successfully removed notification { + notification_id + }" + ) else: - print(f"[NotificationPlugin] Warning: Notification {notification_id} may not have been removed") + print( + f"[NotificationPlugin] Warning: Notification { + notification_id + } may not have been removed" + ) # Trigger immediate refresh using the same pattern as OTP plugin self._trigger_refresh() except Exception as e: - print(f"[NotificationPlugin] Error removing notification {notification_id}: {e}") + print( + f"[NotificationPlugin] Error removing notification {notification_id}: { + e + }" + ) # Still try to refresh even if there was an error self._trigger_refresh() @@ -297,7 +317,11 @@ class NotificationsPlugin(PluginBase): def _on_notification_count_changed(self, service, count: int): """Handle notification count changes (new notifications added or removed).""" try: - print(f"[NotificationPlugin] Notification count changed to {count}, triggering refresh") + print( + f"[NotificationPlugin] Notification count changed to { + count + }, triggering refresh" + ) self._trigger_refresh() except Exception as e: print(f"[NotificationPlugin] Error handling notification count change: {e}") @@ -311,7 +335,7 @@ class NotificationsPlugin(PluginBase): def _show_notification_details(self, notification_data: dict): """Show detailed view of a notification.""" - notif_id = notification_data.get('notification_id', 0) + notif_id = notification_data.get("notification_id", 0) self.showing_detail_for = notif_id self._trigger_refresh() @@ -329,31 +353,38 @@ class NotificationsPlugin(PluginBase): try: notifications = self.notification_service.get_deserialized() for notif in notifications: - notif_data = notif.serialize() if hasattr(notif, 'serialize') else {} - notif_id = notif_data.get('id', 0) + notif_data = ( + notif.serialize() if hasattr(notif, "serialize") else {} + ) + notif_id = notif_data.get("id", 0) if notif_id == self.showing_detail_for: # Create detail widget for this notification detail_data = { - 'notification_id': notif_id, - 'app_name': getattr(notif, 'app_name', 'Unknown'), - 'timestamp': getattr(notif, 'timestamp', time.time()), - 'summary': getattr(notif, 'summary', 'No title'), - 'body': getattr(notif, 'body', '') + "notification_id": notif_id, + "app_name": getattr(notif, "app_name", "Unknown"), + "timestamp": getattr(notif, "timestamp", time.time()), + "summary": getattr(notif, "summary", "No title"), + "body": getattr(notif, "body", ""), } detail_widget = NotificationDetailWidget(detail_data, self) - results.append(Result( - title="Notification Details", - subtitle="Press Escape to go back to list", - icon_markup=icons.notifications, - action=lambda: None, - relevance=1.0, - plugin_name=self.name, - custom_widget=detail_widget, - data={"type": "detail_view", "keep_launcher_open": True} - )) + results.append( + Result( + title="Notification Details", + subtitle="Press Escape to go back to list", + icon_markup=icons.notifications, + action=lambda: None, + relevance=1.0, + plugin_name=self.name, + custom_widget=detail_widget, + data={ + "type": "detail_view", + "keep_launcher_open": True, + }, + ) + ) return results except Exception as e: print(f"Error showing notification details: {e}") @@ -361,72 +392,90 @@ class NotificationsPlugin(PluginBase): # Handle special commands if query_string.lower() in ["clear"]: - results.append(Result( - title="Clear All Notifications", - subtitle="Remove all notifications from history", - description="This will permanently delete all notification history", - icon_markup=icons.trash, - relevance=1.0, - plugin_name=self.name, - action=self._clear_all_notifications - )) + results.append( + Result( + title="Clear All Notifications", + subtitle="Remove all notifications from history", + description="This will permanently delete all notification history", + icon_markup=icons.trash, + relevance=1.0, + plugin_name=self.name, + action=self._clear_all_notifications, + ) + ) return results # Handle DND commands if query_string.lower() in ["off"]: current_dnd = self.notification_service.dont_disturb - results.append(Result( - title="Enable Do Not Disturb", - subtitle="Turn off notification popups" + (" (already enabled)" if current_dnd else ""), - description="Notifications will still be saved to history but won't show popups", - icon_markup=icons.notifications_off, - relevance=1.0, - plugin_name=self.name, - action=lambda: self._toggle_dnd(True), - data={"keep_launcher_open": True} - )) + results.append( + Result( + title="Enable Do Not Disturb", + subtitle="Turn off notification popups" + + (" (already enabled)" if current_dnd else ""), + description="Notifications will still be saved to history but won't show popups", + icon_markup=icons.notifications_off, + relevance=1.0, + plugin_name=self.name, + action=lambda: self._toggle_dnd(True), + data={"keep_launcher_open": True}, + ) + ) return results if query_string.lower() in ["on"]: current_dnd = self.notification_service.dont_disturb - results.append(Result( - title="Disable Do Not Disturb", - subtitle="Turn on notification popups" + (" (already disabled)" if not current_dnd else ""), - description="Notifications will show popups normally", - icon_markup=icons.notifications, - relevance=1.0, - plugin_name=self.name, - action=lambda: self._toggle_dnd(False), - data={"keep_launcher_open": True} - )) + results.append( + Result( + title="Disable Do Not Disturb", + subtitle="Turn on notification popups" + + (" (already disabled)" if not current_dnd else ""), + description="Notifications will show popups normally", + icon_markup=icons.notifications, + relevance=1.0, + plugin_name=self.name, + action=lambda: self._toggle_dnd(False), + data={"keep_launcher_open": True}, + ) + ) return results # Handle remove command - if query_string.lower().startswith("remove ") or query_string.lower().startswith("delete "): - search_term = query_string[7:].strip() if query_string.lower().startswith("remove ") else query_string[7:].strip() + if query_string.lower().startswith( + "remove " + ) or query_string.lower().startswith("delete "): + search_term = ( + query_string[7:].strip() + if query_string.lower().startswith("remove ") + else query_string[7:].strip() + ) if search_term: - results.append(Result( - title=f"Remove notifications containing '{search_term}'", - subtitle="Search for notifications to remove", - description="This will show notifications matching your search term for removal", - icon_markup=icons.trash, - relevance=1.0, - plugin_name=self.name, - action=lambda: None # Will be handled by showing matching notifications - )) + results.append( + Result( + title=f"Remove notifications containing '{search_term}'", + subtitle="Search for notifications to remove", + description="This will show notifications matching your search term for removal", + icon_markup=icons.trash, + relevance=1.0, + plugin_name=self.name, + action=lambda: None, # Will be handled by showing matching notifications + ) + ) try: # Get notifications from service notifications = self.notification_service.get_deserialized() if not notifications: - results.append(Result( - title="No notifications found", - subtitle="Your notification history is empty", - icon_markup=icons.notifications, - relevance=0.5, - plugin_name=self.name - )) + results.append( + Result( + title="No notifications found", + subtitle="Your notification history is empty", + icon_markup=icons.notifications, + relevance=0.5, + plugin_name=self.name, + ) + ) return results # Show count if no query @@ -436,33 +485,39 @@ class NotificationsPlugin(PluginBase): # Add DND status result if dnd_status: - results.append(Result( - title="Do Not Disturb: ON", - subtitle="Notification popups are disabled • Type 'on' to enable", - icon_markup=icons.notifications_off, - relevance=0.9, - plugin_name=self.name, - action=lambda: self._toggle_dnd(False), - data={"keep_launcher_open": True} - )) + results.append( + Result( + title="Do Not Disturb: ON", + subtitle="Notification popups are disabled • Type 'on' to enable", + icon_markup=icons.notifications_off, + relevance=0.9, + plugin_name=self.name, + action=lambda: self._toggle_dnd(False), + data={"keep_launcher_open": True}, + ) + ) else: - results.append(Result( - title="Do Not Disturb: OFF", - subtitle="Notification popups are enabled • Type 'off' to disable", - icon_markup=icons.notifications, - relevance=0.9, - plugin_name=self.name, - action=lambda: self._toggle_dnd(True), - data={"keep_launcher_open": True} - )) + results.append( + Result( + title="Do Not Disturb: OFF", + subtitle="Notification popups are enabled • Type 'off' to disable", + icon_markup=icons.notifications, + relevance=0.9, + plugin_name=self.name, + action=lambda: self._toggle_dnd(True), + data={"keep_launcher_open": True}, + ) + ) - results.append(Result( - title=f"Notification History ({count} notifications)", - subtitle="Type to search notifications or 'clear' to remove all", - icon_markup=icons.notifications, - relevance=0.8, - plugin_name=self.name - )) + results.append( + Result( + title=f"Notification History ({count} notifications)", + subtitle="Type to search notifications or 'clear' to remove all", + icon_markup=icons.notifications, + relevance=0.8, + plugin_name=self.name, + ) + ) # Filter notifications based on query query_lower = query_string.lower() if query_string else "" @@ -470,16 +525,18 @@ class NotificationsPlugin(PluginBase): for notif in notifications: # Get notification data - notif_data = notif.serialize() if hasattr(notif, 'serialize') else {} - notif_id = notif_data.get('id', 0) - timestamp = getattr(notif, 'timestamp', time.time()) + notif_data = notif.serialize() if hasattr(notif, "serialize") else {} + notif_id = notif_data.get("id", 0) + timestamp = getattr(notif, "timestamp", time.time()) # Search in summary, body, and app name - searchable_text = " ".join([ - getattr(notif, 'summary', ''), - getattr(notif, 'body', ''), - getattr(notif, 'app_name', '') - ]).lower() + searchable_text = " ".join( + [ + getattr(notif, "summary", ""), + getattr(notif, "body", ""), + getattr(notif, "app_name", ""), + ] + ).lower() if not query_lower or query_lower in searchable_text: filtered_notifications.append((notif, notif_id, timestamp)) @@ -493,9 +550,9 @@ class NotificationsPlugin(PluginBase): # Create results for notif, notif_id, timestamp in filtered_notifications: - summary = getattr(notif, 'summary', 'No title') - body = getattr(notif, 'body', '') - app_name = getattr(notif, 'app_name', 'Unknown') + summary = getattr(notif, "summary", "No title") + body = getattr(notif, "body", "") + app_name = getattr(notif, "app_name", "Unknown") # Create title and subtitle title = self._truncate_text(summary, 50) @@ -519,25 +576,35 @@ class NotificationsPlugin(PluginBase): icon_markup=icons.notifications if not icon else None, relevance=1.0, plugin_name=self.name, - action=lambda nd={"notification_id": notif_id, "app_name": app_name, "timestamp": timestamp, "summary": summary, "body": body}: self._show_notification_details(nd), + action=lambda nd={ + "notification_id": notif_id, + "app_name": app_name, + "timestamp": timestamp, + "summary": summary, + "body": body, + }: self._show_notification_details(nd), data={ "notification_id": notif_id, "app_name": app_name, "timestamp": timestamp, "can_remove": True, - "alt_action": lambda nid=notif_id: self._clear_notification(nid), - "keep_launcher_open": True - } + "alt_action": lambda nid=notif_id: self._clear_notification( + nid + ), + "keep_launcher_open": True, + }, ) results.append(result) except Exception as e: - results.append(Result( - title="Error loading notifications", - subtitle=str(e), - icon_markup=icons.alert, - relevance=0.0, - plugin_name=self.name - )) + results.append( + Result( + title="Error loading notifications", + subtitle=str(e), + icon_markup=icons.alert, + relevance=0.0, + plugin_name=self.name, + ) + ) return results diff --git a/modules/launcher/plugins/otp.py b/modules/launcher/plugins/otp.py index 9ee9dfa..832f004 100644 --- a/modules/launcher/plugins/otp.py +++ b/modules/launcher/plugins/otp.py @@ -1,20 +1,15 @@ -""" -OTP Manager plugin for the launcher. -Provides TOTP (Time-based One-Time Password) management. -""" - +import base64 import json import subprocess -import time import threading -from typing import List, Dict, Optional +import time from pathlib import Path -import base64 +from typing import Dict, List, Optional +import utils.icons as icons from fabric.utils import get_relative_path from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -import utils.icons as icons try: import pyotp @@ -49,7 +44,7 @@ class OTPPlugin(PluginBase): "pyotp is required for OTP functionality. Install with: pip install pyotp" ) - self.set_triggers(["otp", "otp "]) + self.set_triggers(["otp"]) self._load_secrets() self._ensure_config_file() self._start_refresh_thread() @@ -108,9 +103,10 @@ class OTPPlugin(PluginBase): def _selective_force_refresh(self): """Update time display in existing OTP result items.""" try: - from gi.repository import GLib import gc + from gi.repository import GLib + def do_update(): try: for obj in gc.get_objects(): @@ -811,7 +807,7 @@ class OTPPlugin(PluginBase): def _handle_otpauth_uri(self, account_name: str, uri: str) -> List[Result]: """Handle otpauth:// URI.""" try: - from urllib.parse import urlparse, parse_qs + from urllib.parse import parse_qs, urlparse parsed = urlparse(uri) if parsed.scheme != "otpauth" or parsed.netloc != "totp": diff --git a/modules/launcher/plugins/pomodoro.py b/modules/launcher/plugins/pomodoro.py index a5d2cdf..d8d4e44 100644 --- a/modules/launcher/plugins/pomodoro.py +++ b/modules/launcher/plugins/pomodoro.py @@ -1,16 +1,12 @@ -""" -Pomodoro plugin for the launcher. -Implements the Pomodoro Technique timer with work/break cycles. -""" - +import subprocess import threading import time -import subprocess from typing import List -from modules.launcher.plugin_base import PluginBase -from modules.launcher.result import Result + import utils.icons as icons from gi.repository import GLib +from modules.launcher.plugin_base import PluginBase +from modules.launcher.result import Result class PomodoroTimer: @@ -35,10 +31,19 @@ class PomodoroTimer: def _send_notification_async(self, title, message): """Send a desktop notification asynchronously.""" + def send_notification(): try: subprocess.run( - ["notify-send", "-a", "Pomodoro Timer", "-i", "timer", title, message], + [ + "notify-send", + "-a", + "Pomodoro Timer", + "-i", + "timer", + title, + message, + ], check=False, ) except Exception as e: @@ -50,6 +55,7 @@ class PomodoroTimer: def _send_countdown_notification_async(self, seconds_left): """Send countdown notification asynchronously.""" + def send_countdown(): try: subprocess.run( @@ -224,7 +230,9 @@ class PomodoroTimer: else: period_type = f"Work (Cycle {self.current_cycle})" - self._cached_status = f"{period_type} - {remaining_minutes:02d}:{remaining_seconds:02d} | Total: {self.total_cycles_completed}" + self._cached_status = f"{period_type} - {remaining_minutes:02d}:{ + remaining_seconds:02d + } | Total: {self.total_cycles_completed}" self._last_status_update = current_time def get_status(self): @@ -257,7 +265,7 @@ class PomodoroPlugin(PluginBase): def initialize(self): """Initialize the Pomodoro plugin.""" - self.set_triggers(["pomo", "pomo "]) + self.set_triggers(["pomo"]) def cleanup(self): """Cleanup the Pomodoro plugin.""" @@ -266,6 +274,7 @@ class PomodoroPlugin(PluginBase): def _force_refresh(self): """Force refresh the launcher to update the display.""" try: + def trigger_refresh(): try: # Fallback: try to find launcher instance through other means diff --git a/modules/launcher/plugins/power.py b/modules/launcher/plugins/power.py index 29ae53b..de68dce 100644 --- a/modules/launcher/plugins/power.py +++ b/modules/launcher/plugins/power.py @@ -1,8 +1,3 @@ -""" -Power management plugin for the launcher. -Handles system power operations like shutdown, restart, sleep, etc. -""" - from typing import List import utils.icons as icons @@ -50,7 +45,7 @@ class PowerPlugin(PluginBase): def initialize(self): """Initialize the power plugin.""" - self.set_triggers(["power", "power "]) + self.set_triggers(["power"]) def cleanup(self): """Cleanup the power plugin.""" diff --git a/modules/launcher/plugins/reminders.py b/modules/launcher/plugins/reminders.py index c5bf9c7..bcfbb26 100644 --- a/modules/launcher/plugins/reminders.py +++ b/modules/launcher/plugins/reminders.py @@ -1,17 +1,12 @@ -""" -Reminders plugin for the launcher. -Set time-based reminders with notify-send notifications. -""" - -import threading -import time -import subprocess import re +import subprocess +import threading from datetime import datetime, timedelta -from typing import List, Dict, Optional +from typing import Dict, List, Optional + +import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result -import utils.icons as icons class Reminder: @@ -19,7 +14,13 @@ class Reminder: Represents a single reminder with its timer and metadata. """ - def __init__(self, reminder_id: int, message: str, target_time: datetime, timer: threading.Timer): + def __init__( + self, + reminder_id: int, + message: str, + target_time: datetime, + timer: threading.Timer, + ): self.id = reminder_id self.message = message self.target_time = target_time @@ -70,14 +71,17 @@ class RemindersPlugin(PluginBase): # Regex patterns for time parsing self.time_patterns = { - 'relative_time': re.compile(r'^(\d+)([smhd])$'), # 5m, 30s, 2h, 1d - 'absolute_time': re.compile(r'^(\d{1,2}):(\d{2})$'), # 14:30, 9:15 - 'relative_with_unit': re.compile(r'^(\d+)\s*(min|mins|minute|minutes|hour|hours|sec|seconds|day|days)$', re.IGNORECASE) + "relative_time": re.compile(r"^(\d+)([smhd])$"), # 5m, 30s, 2h, 1d + "absolute_time": re.compile(r"^(\d{1,2}):(\d{2})$"), # 14:30, 9:15 + "relative_with_unit": re.compile( + r"^(\d+)\s*(min|mins|minute|minutes|hour|hours|sec|seconds|day|days)$", + re.IGNORECASE, + ), } def initialize(self): """Initialize the reminders plugin.""" - self.set_triggers(["remind", "remind "]) + self.set_triggers(["remind"]) def cleanup(self): """Cleanup the reminders plugin.""" @@ -108,18 +112,18 @@ class RemindersPlugin(PluginBase): time_str = time_str.strip().lower() # Try relative time format (5m, 30s, 2h, 1d) - match = self.time_patterns['relative_time'].match(time_str) + match = self.time_patterns["relative_time"].match(time_str) if match: value, unit = match.groups() value = int(value) - if unit == 's': + if unit == "s": delta = timedelta(seconds=value) - elif unit == 'm': + elif unit == "m": delta = timedelta(minutes=value) - elif unit == 'h': + elif unit == "h": delta = timedelta(hours=value) - elif unit == 'd': + elif unit == "d": delta = timedelta(days=value) else: return None @@ -127,30 +131,32 @@ class RemindersPlugin(PluginBase): return datetime.now() + delta # Try absolute time format (14:30, 9:15) - match = self.time_patterns['absolute_time'].match(time_str) + match = self.time_patterns["absolute_time"].match(time_str) if match: hour, minute = map(int, match.groups()) if 0 <= hour <= 23 and 0 <= minute <= 59: - target = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0) + target = datetime.now().replace( + hour=hour, minute=minute, second=0, microsecond=0 + ) # If the time has already passed today, schedule for tomorrow if target <= datetime.now(): target += timedelta(days=1) return target # Try relative time with full unit names - match = self.time_patterns['relative_with_unit'].match(time_str) + match = self.time_patterns["relative_with_unit"].match(time_str) if match: value, unit = match.groups() value = int(value) unit = unit.lower() - if unit in ['sec', 'seconds']: + if unit in ["sec", "seconds"]: delta = timedelta(seconds=value) - elif unit in ['min', 'mins', 'minute', 'minutes']: + elif unit in ["min", "mins", "minute", "minutes"]: delta = timedelta(minutes=value) - elif unit in ['hour', 'hours']: + elif unit in ["hour", "hours"]: delta = timedelta(hours=value) - elif unit in ['day', 'days']: + elif unit in ["day", "days"]: delta = timedelta(days=value) else: return None @@ -230,9 +236,13 @@ class RemindersPlugin(PluginBase): reminder = self._create_reminder(time_str, message) if reminder: time_remaining = reminder.get_time_remaining() - self._send_notification("✅ Reminder Created", f"Reminder set for {time_remaining}: {message}") + self._send_notification( + "✅ Reminder Created", f"Reminder set for {time_remaining}: {message}" + ) else: - self._send_notification("❌ Failed to Create Reminder", "The specified time may be in the past") + self._send_notification( + "❌ Failed to Create Reminder", "The specified time may be in the past" + ) def query(self, query_string: str) -> List[Result]: """Process reminder queries.""" @@ -245,7 +255,9 @@ class RemindersPlugin(PluginBase): results.append( Result( title="Reminders Help", - subtitle=f"Active reminders: {active_count} | Usage: remind 5m Take a break", + subtitle=f"Active reminders: { + active_count + } | Usage: remind 5m Take a break", icon_markup=icons.timer_on, action=lambda: None, relevance=1.0, @@ -278,7 +290,7 @@ class RemindersPlugin(PluginBase): return results # Handle list command - if query.lower() in ['list', 'ls', 'show']: + if query.lower() in ["list", "ls", "show"]: if not self.reminders: results.append( Result( @@ -292,7 +304,9 @@ class RemindersPlugin(PluginBase): ) ) else: - for reminder in sorted(self.reminders.values(), key=lambda r: r.target_time): + for reminder in sorted( + self.reminders.values(), key=lambda r: r.target_time + ): time_remaining = reminder.get_time_remaining() target_time = reminder.get_target_time_str() @@ -311,7 +325,7 @@ class RemindersPlugin(PluginBase): return results # Handle cancel command - if query.lower().startswith('cancel') or query.lower().startswith('stop'): + if query.lower().startswith("cancel") or query.lower().startswith("stop"): parts = query.split() if len(parts) == 1: # Cancel all reminders @@ -390,12 +404,19 @@ class RemindersPlugin(PluginBase): results.append( Result( title=f"Set Reminder: {message}", - subtitle=f"Will remind in {time_remaining} (at {target_time_str})", + subtitle=f"Will remind in {time_remaining} (at { + target_time_str + })", icon_markup=icons.timer_on, - action=lambda ts=time_str, msg=message: self._create_and_confirm_reminder(ts, msg), + action=lambda ts=time_str, + msg=message: self._create_and_confirm_reminder(ts, msg), relevance=1.0, plugin_name=self.display_name, - data={"type": "preview", "time_str": time_str, "message": message}, + data={ + "type": "preview", + "time_str": time_str, + "message": message, + }, ) ) else: diff --git a/modules/launcher/plugins/screencapture.py b/modules/launcher/plugins/screencapture.py index 429d11e..57fb64e 100644 --- a/modules/launcher/plugins/screencapture.py +++ b/modules/launcher/plugins/screencapture.py @@ -1,8 +1,3 @@ -""" -Screencapture plugin for the launcher. -Provides screenshot and screen recording functionality using screen-capture.sh script. -""" - import subprocess from typing import List @@ -26,7 +21,7 @@ class ScreencapturePlugin(PluginBase): def initialize(self): """Initialize the screencapture plugin.""" - self.set_triggers(["sc", "sc "]) + self.set_triggers(["sc"]) def cleanup(self): """Cleanup the screencapture plugin.""" @@ -44,7 +39,6 @@ class ScreencapturePlugin(PluginBase): "ss-both": "Take a screenshot of both displays", "screenshot-hdmi": "Take a screenshot of HDMI display", "ss-hdmi": "Take a screenshot of HDMI display", - # Recording commands (with audio) "record": "Start recording main display with audio", "rec": "Start recording main display with audio", @@ -52,7 +46,6 @@ class ScreencapturePlugin(PluginBase): "rec-region": "Start recording selected region with audio", "record-hdmi": "Start recording HDMI display with audio", "rec-hdmi": "Start recording HDMI display with audio", - # Recording commands (no audio) "record-noaudio": "Start recording main display without audio", "rec-noaudio": "Start recording main display without audio", @@ -60,7 +53,6 @@ class ScreencapturePlugin(PluginBase): "rec-noaudio-region": "Start recording selected region without audio", "record-noaudio-hdmi": "Start recording HDMI display without audio", "rec-noaudio-hdmi": "Start recording HDMI display without audio", - # High-quality recording commands "record-hq": "Start high-quality recording of main display", "rec-hq": "Start high-quality recording of main display", @@ -68,16 +60,13 @@ class ScreencapturePlugin(PluginBase): "rec-hq-region": "Start high-quality recording of selected region", "record-hq-hdmi": "Start high-quality recording of HDMI display", "rec-hq-hdmi": "Start high-quality recording of HDMI display", - # GIF recording commands "record-gif": "Start GIF recording of main display", "rec-gif": "Start GIF recording of main display", "record-gif-region": "Start GIF recording of selected region", "rec-gif-region": "Start GIF recording of selected region", - # Control commands "stop": "Stop current recording", - # Conversion commands "convert-webm": "Convert recordings to WebM format", "conv-webm": "Convert recordings to WebM format", @@ -100,9 +89,7 @@ class ScreencapturePlugin(PluginBase): """Check if recording is currently active.""" try: result = subprocess.run( - [self.script_path, "status"], - capture_output=True, - text=True + [self.script_path, "status"], capture_output=True, text=True ) return result.stdout.strip() == "true" except Exception: @@ -111,61 +98,250 @@ class ScreencapturePlugin(PluginBase): def _get_command_result(self, command: str) -> Result: """Get a Result object for a specific command.""" # Import here to avoid circular imports - from utils.icons import ssfull, ssregion, screenshots, screenrecord, stop + from utils.icons import screenrecord, screenshots, ssfull, ssregion, stop command_info = { # Screenshot commands - "screenshot": ("Take Screenshot (eDP-1)", "Capture the main display", ssfull, lambda: self._run_script("screenshot", "eDP-1")), - "ss": ("Take Screenshot (eDP-1)", "Capture the main display", ssfull, lambda: self._run_script("screenshot", "eDP-1")), - "screenshot-region": ("Take Region Screenshot", "Capture a selected region", ssregion, lambda: self._run_script("screenshot", "selection")), - "ss-region": ("Take Region Screenshot", "Capture a selected region", ssregion, lambda: self._run_script("screenshot", "selection")), - "screenshot-both": ("Take Screenshot (Both Displays)", "Capture both displays combined", screenshots, lambda: self._run_script("screenshot", "both")), - "ss-both": ("Take Screenshot (Both Displays)", "Capture both displays combined", screenshots, lambda: self._run_script("screenshot", "both")), - "screenshot-hdmi": ("Take Screenshot (HDMI-A-1)", "Capture HDMI display", ssfull, lambda: self._run_script("screenshot", "HDMI-A-1")), - "ss-hdmi": ("Take Screenshot (HDMI-A-1)", "Capture HDMI display", ssfull, lambda: self._run_script("screenshot", "HDMI-A-1")), - + "screenshot": ( + "Take Screenshot (eDP-1)", + "Capture the main display", + ssfull, + lambda: self._run_script("screenshot", "eDP-1"), + ), + "ss": ( + "Take Screenshot (eDP-1)", + "Capture the main display", + ssfull, + lambda: self._run_script("screenshot", "eDP-1"), + ), + "screenshot-region": ( + "Take Region Screenshot", + "Capture a selected region", + ssregion, + lambda: self._run_script("screenshot", "selection"), + ), + "ss-region": ( + "Take Region Screenshot", + "Capture a selected region", + ssregion, + lambda: self._run_script("screenshot", "selection"), + ), + "screenshot-both": ( + "Take Screenshot (Both Displays)", + "Capture both displays combined", + screenshots, + lambda: self._run_script("screenshot", "both"), + ), + "ss-both": ( + "Take Screenshot (Both Displays)", + "Capture both displays combined", + screenshots, + lambda: self._run_script("screenshot", "both"), + ), + "screenshot-hdmi": ( + "Take Screenshot (HDMI-A-1)", + "Capture HDMI display", + ssfull, + lambda: self._run_script("screenshot", "HDMI-A-1"), + ), + "ss-hdmi": ( + "Take Screenshot (HDMI-A-1)", + "Capture HDMI display", + ssfull, + lambda: self._run_script("screenshot", "HDMI-A-1"), + ), # Recording commands (with audio) - "record": ("Start Recording (eDP-1)", "Record the main display with audio", screenrecord, lambda: self._run_script("record", "eDP-1")), - "rec": ("Start Recording (eDP-1)", "Record the main display with audio", screenrecord, lambda: self._run_script("record", "eDP-1")), - "record-region": ("Start Region Recording", "Record a selected region with audio", screenrecord, lambda: self._run_script("record", "selection")), - "rec-region": ("Start Region Recording", "Record a selected region with audio", screenrecord, lambda: self._run_script("record", "selection")), - "record-hdmi": ("Start Recording (HDMI-A-1)", "Record HDMI display with audio", screenrecord, lambda: self._run_script("record", "HDMI-A-1")), - "rec-hdmi": ("Start Recording (HDMI-A-1)", "Record HDMI display with audio", screenrecord, lambda: self._run_script("record", "HDMI-A-1")), - + "record": ( + "Start Recording (eDP-1)", + "Record the main display with audio", + screenrecord, + lambda: self._run_script("record", "eDP-1"), + ), + "rec": ( + "Start Recording (eDP-1)", + "Record the main display with audio", + screenrecord, + lambda: self._run_script("record", "eDP-1"), + ), + "record-region": ( + "Start Region Recording", + "Record a selected region with audio", + screenrecord, + lambda: self._run_script("record", "selection"), + ), + "rec-region": ( + "Start Region Recording", + "Record a selected region with audio", + screenrecord, + lambda: self._run_script("record", "selection"), + ), + "record-hdmi": ( + "Start Recording (HDMI-A-1)", + "Record HDMI display with audio", + screenrecord, + lambda: self._run_script("record", "HDMI-A-1"), + ), + "rec-hdmi": ( + "Start Recording (HDMI-A-1)", + "Record HDMI display with audio", + screenrecord, + lambda: self._run_script("record", "HDMI-A-1"), + ), # Recording commands (no audio) - "record-noaudio": ("Start Recording No Audio (eDP-1)", "Record the main display without audio", screenrecord, lambda: self._run_script("record-noaudio", "eDP-1")), - "rec-noaudio": ("Start Recording No Audio (eDP-1)", "Record the main display without audio", screenrecord, lambda: self._run_script("record-noaudio", "eDP-1")), - "record-noaudio-region": ("Start Region Recording No Audio", "Record a selected region without audio", screenrecord, lambda: self._run_script("record-noaudio", "selection")), - "rec-noaudio-region": ("Start Region Recording No Audio", "Record a selected region without audio", screenrecord, lambda: self._run_script("record-noaudio", "selection")), - "record-noaudio-hdmi": ("Start Recording No Audio (HDMI-A-1)", "Record HDMI display without audio", screenrecord, lambda: self._run_script("record-noaudio", "HDMI-A-1")), - "rec-noaudio-hdmi": ("Start Recording No Audio (HDMI-A-1)", "Record HDMI display without audio", screenrecord, lambda: self._run_script("record-noaudio", "HDMI-A-1")), - + "record-noaudio": ( + "Start Recording No Audio (eDP-1)", + "Record the main display without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "eDP-1"), + ), + "rec-noaudio": ( + "Start Recording No Audio (eDP-1)", + "Record the main display without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "eDP-1"), + ), + "record-noaudio-region": ( + "Start Region Recording No Audio", + "Record a selected region without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "selection"), + ), + "rec-noaudio-region": ( + "Start Region Recording No Audio", + "Record a selected region without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "selection"), + ), + "record-noaudio-hdmi": ( + "Start Recording No Audio (HDMI-A-1)", + "Record HDMI display without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "HDMI-A-1"), + ), + "rec-noaudio-hdmi": ( + "Start Recording No Audio (HDMI-A-1)", + "Record HDMI display without audio", + screenrecord, + lambda: self._run_script("record-noaudio", "HDMI-A-1"), + ), # High-quality recording commands - "record-hq": ("Start HQ Recording (eDP-1)", "High-quality recording for YouTube", screenrecord, lambda: self._run_script("record-hq", "eDP-1")), - "rec-hq": ("Start HQ Recording (eDP-1)", "High-quality recording for YouTube", screenrecord, lambda: self._run_script("record-hq", "eDP-1")), - "record-hq-region": ("Start HQ Region Recording", "High-quality region recording", screenrecord, lambda: self._run_script("record-hq", "selection")), - "rec-hq-region": ("Start HQ Region Recording", "High-quality region recording", screenrecord, lambda: self._run_script("record-hq", "selection")), - "record-hq-hdmi": ("Start HQ Recording (HDMI-A-1)", "High-quality HDMI recording", screenrecord, lambda: self._run_script("record-hq", "HDMI-A-1")), - "rec-hq-hdmi": ("Start HQ Recording (HDMI-A-1)", "High-quality HDMI recording", screenrecord, lambda: self._run_script("record-hq", "HDMI-A-1")), - + "record-hq": ( + "Start HQ Recording (eDP-1)", + "High-quality recording for YouTube", + screenrecord, + lambda: self._run_script("record-hq", "eDP-1"), + ), + "rec-hq": ( + "Start HQ Recording (eDP-1)", + "High-quality recording for YouTube", + screenrecord, + lambda: self._run_script("record-hq", "eDP-1"), + ), + "record-hq-region": ( + "Start HQ Region Recording", + "High-quality region recording", + screenrecord, + lambda: self._run_script("record-hq", "selection"), + ), + "rec-hq-region": ( + "Start HQ Region Recording", + "High-quality region recording", + screenrecord, + lambda: self._run_script("record-hq", "selection"), + ), + "record-hq-hdmi": ( + "Start HQ Recording (HDMI-A-1)", + "High-quality HDMI recording", + screenrecord, + lambda: self._run_script("record-hq", "HDMI-A-1"), + ), + "rec-hq-hdmi": ( + "Start HQ Recording (HDMI-A-1)", + "High-quality HDMI recording", + screenrecord, + lambda: self._run_script("record-hq", "HDMI-A-1"), + ), # GIF recording commands - "record-gif": ("Start GIF Recording (eDP-1)", "Record as optimized GIF", screenrecord, lambda: self._run_script("record-gif", "eDP-1")), - "rec-gif": ("Start GIF Recording (eDP-1)", "Record as optimized GIF", screenrecord, lambda: self._run_script("record-gif", "eDP-1")), - "record-gif-region": ("Start GIF Region Recording", "Record selected region as GIF", screenrecord, lambda: self._run_script("record-gif", "selection")), - "rec-gif-region": ("Start GIF Region Recording", "Record selected region as GIF", screenrecord, lambda: self._run_script("record-gif", "selection")), - + "record-gif": ( + "Start GIF Recording (eDP-1)", + "Record as optimized GIF", + screenrecord, + lambda: self._run_script("record-gif", "eDP-1"), + ), + "rec-gif": ( + "Start GIF Recording (eDP-1)", + "Record as optimized GIF", + screenrecord, + lambda: self._run_script("record-gif", "eDP-1"), + ), + "record-gif-region": ( + "Start GIF Region Recording", + "Record selected region as GIF", + screenrecord, + lambda: self._run_script("record-gif", "selection"), + ), + "rec-gif-region": ( + "Start GIF Region Recording", + "Record selected region as GIF", + screenrecord, + lambda: self._run_script("record-gif", "selection"), + ), # Control commands - "stop": ("Stop Recording", "Stop the current screen recording", stop, lambda: self._run_script("record", "stop")), - + "stop": ( + "Stop Recording", + "Stop the current screen recording", + stop, + lambda: self._run_script("record", "stop"), + ), # Conversion commands - "convert-webm": ("Convert to WebM", "Convert MKV recordings to WebM format", screenrecord, lambda: self._run_script("convert", "webm")), - "conv-webm": ("Convert to WebM", "Convert MKV recordings to WebM format", screenrecord, lambda: self._run_script("convert", "webm")), - "convert-iphone": ("Convert for iPhone", "Convert recordings for iPhone compatibility", screenrecord, lambda: self._run_script("convert", "iphone")), - "conv-iphone": ("Convert for iPhone", "Convert recordings for iPhone compatibility", screenrecord, lambda: self._run_script("convert", "iphone")), - "convert-youtube": ("Convert for YouTube", "Convert recordings for YouTube upload", screenrecord, lambda: self._run_script("convert", "youtube")), - "conv-youtube": ("Convert for YouTube", "Convert recordings for YouTube upload", screenrecord, lambda: self._run_script("convert", "youtube")), - "convert-gif": ("Convert to GIF", "Convert video recordings to GIF", screenrecord, lambda: self._run_script("convert", "gif")), - "conv-gif": ("Convert to GIF", "Convert video recordings to GIF", screenrecord, lambda: self._run_script("convert", "gif")), + "convert-webm": ( + "Convert to WebM", + "Convert MKV recordings to WebM format", + screenrecord, + lambda: self._run_script("convert", "webm"), + ), + "conv-webm": ( + "Convert to WebM", + "Convert MKV recordings to WebM format", + screenrecord, + lambda: self._run_script("convert", "webm"), + ), + "convert-iphone": ( + "Convert for iPhone", + "Convert recordings for iPhone compatibility", + screenrecord, + lambda: self._run_script("convert", "iphone"), + ), + "conv-iphone": ( + "Convert for iPhone", + "Convert recordings for iPhone compatibility", + screenrecord, + lambda: self._run_script("convert", "iphone"), + ), + "convert-youtube": ( + "Convert for YouTube", + "Convert recordings for YouTube upload", + screenrecord, + lambda: self._run_script("convert", "youtube"), + ), + "conv-youtube": ( + "Convert for YouTube", + "Convert recordings for YouTube upload", + screenrecord, + lambda: self._run_script("convert", "youtube"), + ), + "convert-gif": ( + "Convert to GIF", + "Convert video recordings to GIF", + screenrecord, + lambda: self._run_script("convert", "gif"), + ), + "conv-gif": ( + "Convert to GIF", + "Convert video recordings to GIF", + screenrecord, + lambda: self._run_script("convert", "gif"), + ), } if command in command_info: diff --git a/modules/launcher/plugins/system.py b/modules/launcher/plugins/system.py index bcf4cef..af5fa07 100644 --- a/modules/launcher/plugins/system.py +++ b/modules/launcher/plugins/system.py @@ -1,8 +1,3 @@ -""" -System plugin for the launcher. -Provides system commands and actions. -""" - import os import time import shlex @@ -31,8 +26,7 @@ class SystemPlugin(PluginBase): def initialize(self): """Initialize the system plugin.""" - # Set up triggers for system commands - self.set_triggers(["bin", "bin "]) + self.set_triggers(["bin"]) self._update_bin_cache() def cleanup(self): @@ -124,8 +118,10 @@ class SystemPlugin(PluginBase): def _create_action(self, command: Union[str, List[str]]): """Create an action function for the given command.""" + def action(): self._execute_command(command) + return action def _execute_command(self, command: Union[str, List[str]]): @@ -141,5 +137,5 @@ class SystemPlugin(PluginBase): except Exception as e: print(f"SystemPlugin: Error executing command '{command}': {e}") import traceback - traceback.print_exc() + traceback.print_exc() diff --git a/modules/launcher/plugins/wallpaper.py b/modules/launcher/plugins/wallpaper.py index ad6b78c..a40ca51 100644 --- a/modules/launcher/plugins/wallpaper.py +++ b/modules/launcher/plugins/wallpaper.py @@ -1,24 +1,20 @@ -""" -Wallpaper plugin for the launcher. -Provides wallpaper management with search, random selection, matugen integration, and hex color support. -""" - +import colorsys +import hashlib +import json import os import random -import hashlib -import colorsys import re -import json -from typing import List, Dict, Optional -from PIL import Image -from gi.repository import GdkPixbuf, GLib -from modules.launcher.plugin_base import PluginBase -from modules.launcher.result import Result -from fabric.utils.helpers import exec_shell_command_async -import config.data as data -import utils.icons as icons 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): @@ -46,7 +42,7 @@ class WallpaperPlugin(PluginBase): def initialize(self): """Initialize the wallpaper plugin.""" - self.set_triggers(["wall", "wall "]) + self.set_triggers(["wall"]) self._load_wallpapers() os.makedirs(self.cache_dir, exist_ok=True) # Start background thumbnail creation @@ -78,7 +74,7 @@ class WallpaperPlugin(PluginBase): """Get current matugen state from config.json.""" self.matugen_enabled = True # Default to True try: - with open(data.CONFIG_FILE, 'r') as f: + with open(data.CONFIG_FILE, "r") as f: config = json.load(f) self.matugen_enabled = config.get("matugen_enabled", True) except FileNotFoundError: @@ -99,26 +95,26 @@ class WallpaperPlugin(PluginBase): # Read current config config = {} if os.path.exists(data.CONFIG_FILE): - with open(data.CONFIG_FILE, 'r') as f: + 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: + 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" - ) + # # 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.""" @@ -144,6 +140,7 @@ class WallpaperPlugin(PluginBase): 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: @@ -198,6 +195,7 @@ class WallpaperPlugin(PluginBase): # 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) @@ -240,23 +238,12 @@ class WallpaperPlugin(PluginBase): matugen_enabled = self._get_matugen_state() if matugen_enabled: exec_shell_command_async(f'matugen image "{full_path}" -t {scheme}') - scheme_name = self.schemes.get(scheme, scheme) - exec_shell_command_async( - f"notify-send '🖼️ Wallpaper Applied' 'File: {filename}\\nScheme: {scheme_name}' -a '{ - data.APP_NAME_CAP - }' -e" - ) else: exec_shell_command_async( f'swww img "{ full_path }" -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest' ) - exec_shell_command_async( - f"notify-send '🖼️ Wallpaper Applied' 'File: {filename}\\nMatugen: Disabled' -a '{ - data.APP_NAME_CAP - }' -e" - ) def _set_random_wallpaper(self): """Set a random wallpaper.""" @@ -298,20 +285,6 @@ class WallpaperPlugin(PluginBase): scheme_name = self.schemes.get(scheme, scheme) matugen_enabled = self._get_matugen_state() - # Show notification with matugen status - if matugen_enabled: - exec_shell_command_async( - f"notify-send '🎨 Color Scheme' 'Set to { - scheme_name - }\\nMatugen: Enabled' -a '{data.APP_NAME_CAP}' -e" - ) - else: - exec_shell_command_async( - f"notify-send '⚠️ Color Scheme' 'Set to { - scheme_name - }\\nMatugen: Disabled (enable for effect)' -a '{data.APP_NAME_CAP}' -e" - ) - def _apply_hex_color(self, hex_color: str, scheme: str = None): """Apply hex color using matugen. Assumes matugen is enabled.""" if not hex_color.startswith("#"): @@ -321,12 +294,6 @@ class WallpaperPlugin(PluginBase): scheme = self._get_current_scheme() exec_shell_command_async(f'matugen color hex "{hex_color}" -t {scheme}') - scheme_name = self.schemes.get(scheme, scheme) - exec_shell_command_async( - f"notify-send '🎨 Hex Color Applied' 'Color: {hex_color}\\nScheme: { - scheme_name - }' -a '{data.APP_NAME_CAP}' -e" - ) def _generate_random_hex_color(self) -> str: """Generate a random hex color.""" @@ -446,10 +413,15 @@ class WallpaperPlugin(PluginBase): # 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}", + 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(c, s), + action=lambda c=hex_color, + s=scheme: self._apply_hex_color(c, s), relevance=1.0, plugin_name=self.display_name, data={ @@ -465,8 +437,10 @@ class WallpaperPlugin(PluginBase): # Show error result when matugen is disabled results.append( Result( - title=f"Cannot Apply Hex Color: {hex_color}{indicator_text}", - subtitle=f"Matugen is disabled • Enable matugen to use hex colors", + title=f"Cannot Apply Hex Color: {hex_color}{ + indicator_text + }", + subtitle="Matugen is disabled • Enable matugen to use hex colors", icon_markup=icons.palette, action=lambda: None, relevance=1.0, @@ -484,9 +458,14 @@ class WallpaperPlugin(PluginBase): results.append( Result( title=f"Apply Hex Color: {hex_color}{indicator_text}", - subtitle=f"Apply with {scheme_name} scheme • {status_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(c, s) if matugen_enabled else None, + action=lambda c=hex_color, + s=scheme: self._apply_hex_color(c, s) + if matugen_enabled + else None, relevance=0.9, plugin_name=self.display_name, data={ @@ -503,7 +482,7 @@ class WallpaperPlugin(PluginBase): results.append( Result( title=f"Hex Color (incomplete): {hex_color}", - subtitle=f"Complete the 6-digit hex color to apply", + subtitle="Complete the 6-digit hex color to apply", icon_markup=icons.palette, action=lambda: None, relevance=0.7, @@ -517,10 +496,12 @@ class WallpaperPlugin(PluginBase): ) 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"): + 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) @@ -529,9 +510,13 @@ class WallpaperPlugin(PluginBase): results.append( Result( title=f"Random Hex Color{indicator_text}", - subtitle=f"Generate and apply random color with {scheme_name} scheme • {status_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(self._generate_random_hex_color(), s), + action=lambda s=scheme: self._apply_hex_color( + self._generate_random_hex_color(), s + ), relevance=1.0, plugin_name=self.display_name, data={ @@ -547,7 +532,7 @@ class WallpaperPlugin(PluginBase): results.append( Result( title=f"Cannot Apply Random Color{indicator_text}", - subtitle=f"Matugen is disabled • Enable matugen to use hex colors", + subtitle="Matugen is disabled • Enable matugen to use hex colors", icon_markup=icons.palette, action=lambda: None, relevance=1.0, @@ -565,7 +550,11 @@ class WallpaperPlugin(PluginBase): title=f"Random Hex Color{indicator_text}", subtitle=f"Generate and apply random color • {status_text}", icon_markup=icons.palette, - action=lambda: self._apply_hex_color(self._generate_random_hex_color(), scheme) if matugen_enabled else None, + action=lambda: self._apply_hex_color( + self._generate_random_hex_color(), scheme + ) + if matugen_enabled + else None, relevance=0.8, plugin_name=self.display_name, data={ @@ -679,8 +668,12 @@ class WallpaperPlugin(PluginBase): 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}", + 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, @@ -692,7 +685,9 @@ class WallpaperPlugin(PluginBase): }, ) ) - elif ("on" in query or "enable" in query) and not query.strip().endswith(("on", "enable")): + elif ("on" in query or "enable" in query) and not query.strip().endswith( + ("on", "enable") + ): # Show suggestion for enabling (partial match) results.append( Result( @@ -709,7 +704,9 @@ class WallpaperPlugin(PluginBase): }, ) ) - elif ("off" in query or "disable" in query) and not query.strip().endswith(("off", "disable")): + elif ("off" in query or "disable" in query) and not query.strip().endswith( + ("off", "disable") + ): # Show suggestion for disabling (partial match) results.append( Result( diff --git a/modules/launcher/plugins/websearch.py b/modules/launcher/plugins/websearch.py index a308a80..edbf54b 100644 --- a/modules/launcher/plugins/websearch.py +++ b/modules/launcher/plugins/websearch.py @@ -1,11 +1,6 @@ -""" -Web Search plugin for the launcher. -Allows searching the web using various search engines. -""" - import subprocess import urllib.parse -from typing import Dict, List +from typing import List import utils.icons as icons from modules.launcher.plugin_base import PluginBase @@ -81,7 +76,7 @@ class WebSearchPlugin(PluginBase): def initialize(self): """Initialize the web search plugin.""" # Set up triggers for web search - triggers = ["web", "web ", "?", "? "] + triggers = ["web", "?"] # Add search engine specific triggers for engine in self.search_engines.keys():