From 56a5a738df078cf71f7a50cd3595636037d58257 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 29 Jun 2025 11:21:05 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85=20style(launcher,=20osd,=20switche?= =?UTF-8?q?r):=20update=20spacing=20and=20background=20for=20better=20cons?= =?UTF-8?q?istency=20with=20GNOME=20design=20guidelines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- memory_monitor.py | 383 +++++++++ modules/launcher/plugins/archive/files.py | 407 ---------- .../launcher/plugins/archive/notifications.py | 611 -------------- modules/launcher/plugins/archive/pomodoro.py | 422 ---------- .../launcher/plugins/archive/screentime.py | 764 ------------------ styles/launcher.css | 187 ----- styles/osd.css | 2 +- styles/switcher.css | 6 +- 8 files changed, 385 insertions(+), 2397 deletions(-) create mode 100755 memory_monitor.py delete mode 100644 modules/launcher/plugins/archive/files.py delete mode 100644 modules/launcher/plugins/archive/notifications.py delete mode 100644 modules/launcher/plugins/archive/pomodoro.py delete mode 100644 modules/launcher/plugins/archive/screentime.py diff --git a/memory_monitor.py b/memory_monitor.py new file mode 100755 index 0000000..0354d2b --- /dev/null +++ b/memory_monitor.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +""" +Memory Monitor Script for Modus Components +Shows memory usage for each component in the modules/ directory. +""" + +import os +import sys +import time +import psutil +import subprocess +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass +import json +import argparse + + +@dataclass +class ComponentMemory: + """Data class to store component memory information.""" + name: str + pid: Optional[int] + memory_mb: float + memory_percent: float + cpu_percent: float + status: str + children_count: int = 0 + children_memory_mb: float = 0.0 + + +class ModusMemoryMonitor: + """Monitor memory usage of Modus components.""" + + def __init__(self): + self.modus_processes = {} + self.component_mapping = { + # Main components from modules/ + 'dock': ['dock', 'Dock'], + 'launcher': ['launcher', 'Launcher'], + 'notification_popup': ['notification', 'NotificationPopup'], + 'osd': ['osd', 'OSD'], + 'switcher': ['switcher', 'ApplicationSwitcher'], + 'corners': ['corners', 'Corners'], + + # Dock components + 'dock_applications': ['applications'], + 'dock_battery': ['battery'], + 'dock_controls': ['controls'], + 'dock_indicators': ['indicators'], + 'dock_metrics': ['metrics'], + 'dock_music_player': ['music_player', 'music'], + 'dock_notifications': ['dock_notifications'], + 'dock_workspaces': ['workspaces'], + + # Launcher plugins + 'plugin_applications': ['plugin_applications'], + 'plugin_bluetooth': ['plugin_bluetooth'], + 'plugin_bookmarks': ['plugin_bookmarks'], + 'plugin_caffeine': ['plugin_caffeine'], + 'plugin_calculator': ['plugin_calculator'], + 'plugin_calendar': ['plugin_calendar'], + 'plugin_clipboard': ['plugin_clipboard'], + 'plugin_emoji': ['plugin_emoji'], + 'plugin_kanban': ['plugin_kanban'], + 'plugin_network': ['plugin_network'], + 'plugin_otp': ['plugin_otp'], + 'plugin_password': ['plugin_password'], + 'plugin_power': ['plugin_power'], + 'plugin_process': ['plugin_process'], + 'plugin_reminders': ['plugin_reminders'], + 'plugin_screencapture': ['plugin_screencapture'], + 'plugin_system': ['plugin_system'], + 'plugin_wallpaper': ['plugin_wallpaper'], + 'plugin_websearch': ['plugin_websearch'], + + # Services + 'service_brightness': ['brightness'], + 'service_network': ['network'], + 'service_battery': ['battery_service'], + 'service_mpris': ['mpris'], + 'service_notification': ['notification_service'], + } + + def find_modus_processes(self) -> List[psutil.Process]: + """Find all processes related to Modus.""" + modus_processes = [] + + try: + for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'ppid']): + try: + info = proc.info + cmdline = ' '.join(info['cmdline']) if info['cmdline'] else '' + + # Check if this is a Modus-related process + if (info['name'] == 'modus' or + 'main.py' in cmdline or + 'Modus' in cmdline or + any(module in cmdline.lower() for module in ['dock', 'launcher', 'notification'])): + modus_processes.append(proc) + + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + except Exception as e: + print(f"Error finding Modus processes: {e}") + + return modus_processes + + def get_process_memory_info(self, proc: psutil.Process) -> Tuple[float, float, float]: + """Get memory and CPU information for a process.""" + try: + memory_info = proc.memory_info() + memory_mb = memory_info.rss / (1024 * 1024) + memory_percent = proc.memory_percent() + cpu_percent = proc.cpu_percent() + return memory_mb, memory_percent, cpu_percent + except (psutil.NoSuchProcess, psutil.AccessDenied): + return 0.0, 0.0, 0.0 + + def categorize_process(self, proc: psutil.Process) -> str: + """Categorize a process based on its command line and name.""" + try: + info = proc.info + cmdline = ' '.join(info['cmdline']) if info['cmdline'] else '' + name = info['name'].lower() + + # Check for main process + if 'main.py' in cmdline: + return 'main_process' + + # Check for specific components based on command line or imports + for component, keywords in self.component_mapping.items(): + if any(keyword.lower() in cmdline.lower() or keyword.lower() in name for keyword in keywords): + return component + + # Default categorization + if 'python' in name: + return 'python_subprocess' + else: + return 'other' + + except (psutil.NoSuchProcess, psutil.AccessDenied): + return 'unknown' + + def get_component_memory_usage(self) -> Dict[str, ComponentMemory]: + """Get memory usage for each Modus component.""" + components = {} + modus_processes = self.find_modus_processes() + + if not modus_processes: + print("No Modus processes found. Make sure Modus is running.") + return components + + # Initialize CPU monitoring for better readings + for proc in modus_processes: + try: + proc.cpu_percent() + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + # Wait a bit for CPU percentage calculation + time.sleep(0.1) + + for proc in modus_processes: + try: + category = self.categorize_process(proc) + memory_mb, memory_percent, cpu_percent = self.get_process_memory_info(proc) + + if category not in components: + components[category] = ComponentMemory( + name=category, + pid=proc.pid, + memory_mb=memory_mb, + memory_percent=memory_percent, + cpu_percent=cpu_percent, + status=proc.status(), + children_count=0, + children_memory_mb=0.0 + ) + else: + # Aggregate memory for components with multiple processes + existing = components[category] + existing.memory_mb += memory_mb + existing.memory_percent += memory_percent + existing.cpu_percent += cpu_percent + existing.children_count += 1 + existing.children_memory_mb += memory_mb + + # Also check for child processes + try: + children = proc.children(recursive=True) + for child in children: + child_memory_mb, child_memory_percent, child_cpu_percent = self.get_process_memory_info(child) + components[category].children_memory_mb += child_memory_mb + components[category].memory_mb += child_memory_mb + components[category].memory_percent += child_memory_percent + components[category].cpu_percent += child_cpu_percent + components[category].children_count += 1 + + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + return components + + def get_module_file_sizes(self) -> Dict[str, float]: + """Get file sizes for modules in the modules/ directory.""" + module_sizes = {} + modules_dir = os.path.join(os.path.dirname(__file__), 'modules') + + if not os.path.exists(modules_dir): + return module_sizes + + try: + for root, _, files in os.walk(modules_dir): + for file in files: + if file.endswith('.py'): + file_path = os.path.join(root, file) + try: + size_kb = os.path.getsize(file_path) / 1024 + relative_path = os.path.relpath(file_path, modules_dir) + module_sizes[relative_path] = size_kb + except OSError: + continue + except Exception as e: + print(f"Error getting module file sizes: {e}") + + return module_sizes + + def print_memory_report(self, components: Dict[str, ComponentMemory], + module_sizes: Dict[str, float], + format_type: str = 'table'): + """Print a formatted memory usage report.""" + if not components: + print("No Modus components found running.") + return + + total_memory = sum(comp.memory_mb for comp in components.values()) + total_cpu = sum(comp.cpu_percent for comp in components.values()) + + if format_type == 'json': + self._print_json_report(components, module_sizes, total_memory, total_cpu) + else: + self._print_table_report(components, module_sizes, total_memory, total_cpu) + + def _print_table_report(self, components: Dict[str, ComponentMemory], + module_sizes: Dict[str, float], + total_memory: float, total_cpu: float): + """Print memory report in table format.""" + print("\n" + "="*80) + print("MODUS MEMORY USAGE REPORT") + print("="*80) + + # Sort components by memory usage + sorted_components = sorted(components.items(), key=lambda x: x[1].memory_mb, reverse=True) + + print(f"\n{'Component':<25} {'Memory (MB)':<12} {'Memory %':<10} {'CPU %':<8} {'PID':<8} {'Status':<10}") + print("-" * 80) + + for name, comp in sorted_components: + print(f"{name:<25} {comp.memory_mb:>8.2f} {comp.memory_percent:>6.2f}% " + f"{comp.cpu_percent:>5.2f}% {comp.pid or 'N/A':<8} {comp.status:<10}") + + if comp.children_count > 0: + print(f" └─ Children: {comp.children_count} processes, " + f"{comp.children_memory_mb:.2f} MB") + + print("-" * 80) + print(f"{'TOTAL':<25} {total_memory:>8.2f} {'-':<10} {total_cpu:>5.2f}%") + + # Module file sizes + if module_sizes: + print(f"\n{'MODULE FILE SIZES':<25} {'Size (KB)':<12}") + print("-" * 40) + sorted_modules = sorted(module_sizes.items(), key=lambda x: x[1], reverse=True) + for module, size in sorted_modules[:10]: # Top 10 largest files + print(f"{module:<25} {size:>8.2f}") + + print("\n" + "="*80) + + def _print_json_report(self, components: Dict[str, ComponentMemory], + module_sizes: Dict[str, float], + total_memory: float, total_cpu: float): + """Print memory report in JSON format.""" + report = { + 'timestamp': time.time(), + 'total_memory_mb': round(total_memory, 2), + 'total_cpu_percent': round(total_cpu, 2), + 'components': {}, + 'module_file_sizes_kb': module_sizes + } + + for name, comp in components.items(): + report['components'][name] = { + 'memory_mb': round(comp.memory_mb, 2), + 'memory_percent': round(comp.memory_percent, 2), + 'cpu_percent': round(comp.cpu_percent, 2), + 'pid': comp.pid, + 'status': comp.status, + 'children_count': comp.children_count, + 'children_memory_mb': round(comp.children_memory_mb, 2) + } + + print(json.dumps(report, indent=2)) + + def monitor_continuously(self, interval: int = 5, format_type: str = 'table'): + """Monitor memory usage continuously.""" + try: + while True: + os.system('clear' if os.name == 'posix' else 'cls') + components = self.get_component_memory_usage() + module_sizes = self.get_module_file_sizes() + self.print_memory_report(components, module_sizes, format_type) + print(f"\nRefreshing every {interval} seconds... (Press Ctrl+C to stop)") + time.sleep(interval) + except KeyboardInterrupt: + print("\nMonitoring stopped.") + + def get_memory_summary(self) -> Dict[str, float]: + """Get a summary of memory usage by category.""" + components = self.get_component_memory_usage() + + summary = { + 'main_components': 0.0, + 'dock_components': 0.0, + 'launcher_plugins': 0.0, + 'services': 0.0, + 'other': 0.0 + } + + for name, comp in components.items(): + if name in ['main_process', 'dock', 'launcher', 'notification_popup', 'osd', 'switcher', 'corners']: + summary['main_components'] += comp.memory_mb + elif name.startswith('dock_'): + summary['dock_components'] += comp.memory_mb + elif name.startswith('plugin_'): + summary['launcher_plugins'] += comp.memory_mb + elif name.startswith('service_'): + summary['services'] += comp.memory_mb + else: + summary['other'] += comp.memory_mb + + return summary + + +def main(): + """Main function to run the memory monitor.""" + parser = argparse.ArgumentParser(description='Monitor memory usage of Modus components') + parser.add_argument('--format', choices=['table', 'json'], default='table', + help='Output format (default: table)') + parser.add_argument('--monitor', action='store_true', + help='Monitor continuously') + parser.add_argument('--interval', type=int, default=5, + help='Refresh interval in seconds for continuous monitoring (default: 5)') + parser.add_argument('--summary', action='store_true', + help='Show only summary by category') + + args = parser.parse_args() + + monitor = ModusMemoryMonitor() + + if args.monitor: + monitor.monitor_continuously(args.interval, args.format) + elif args.summary: + summary = monitor.get_memory_summary() + print("\nMODUS MEMORY SUMMARY BY CATEGORY") + print("="*40) + total = sum(summary.values()) + for category, memory in summary.items(): + percentage = (memory / total * 100) if total > 0 else 0 + print(f"{category.replace('_', ' ').title():<20} {memory:>8.2f} MB ({percentage:>5.1f}%)") + print("-"*40) + print(f"{'Total':<20} {total:>8.2f} MB") + else: + components = monitor.get_component_memory_usage() + module_sizes = monitor.get_module_file_sizes() + monitor.print_memory_report(components, module_sizes, args.format) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/modules/launcher/plugins/archive/files.py b/modules/launcher/plugins/archive/files.py deleted file mode 100644 index b654969..0000000 --- a/modules/launcher/plugins/archive/files.py +++ /dev/null @@ -1,407 +0,0 @@ -import os -import subprocess -import threading -import time -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 - - -class FilesPlugin(PluginBase): - """ - High-performance plugin for searching files only (excludes directories). - Uses advanced fuzzy matching, caching, and threading for optimal performance. - """ - - def __init__(self): - super().__init__() - self.display_name = "Files" - self.description = "Search for files only" - - # Performance-optimized search paths - focus on user directories - self.search_paths = [ - os.path.expanduser("~/Documents"), - os.path.expanduser("~/Downloads"), - os.path.expanduser("~/Desktop"), - os.path.expanduser("~/Pictures"), - os.path.expanduser("~/Videos"), - os.path.expanduser("~/Music"), - os.path.expanduser("~"), # Home directory but with limited depth - ] - - # Performance settings - self.max_results = 15 - self.max_depth = 2 # Reduced depth for better performance - self.search_timeout = 0.5 # Reduced timeout - self.min_query_length = 1 # Allow single character searches - - # Fuzzy search settings - optimized for performance - self.fuzzy_thresholds = { - "ratio": 70, # Increased for better precision - "partial": 80, # Increased for better precision - "token_sort": 75, # Increased for better precision - "token_set": 75, # Increased for better precision - } - - # Enhanced caching - self.file_cache: Dict[str, List[str]] = {} - self.cache_timestamp: Dict[str, float] = {} - self.cache_ttl = 300 - self.cache_lock = threading.Lock() - - # Thread pool for concurrent searches - self.executor = ThreadPoolExecutor( - max_workers=4, thread_name_prefix="files-search" - ) - - def initialize(self): - """Initialize the files plugin.""" - self.set_triggers( - [ - "file", - "find", - ] - ) - self._warm_cache() - - def cleanup(self): - """Cleanup the files plugin.""" - if hasattr(self, "executor"): - self.executor.shutdown(wait=False) - with self.cache_lock: - self.file_cache.clear() - self.cache_timestamp.clear() - - def query(self, query_string: str) -> List[Result]: - """Fast search for files matching the query.""" - query = query_string.strip() - - if len(query) < self.min_query_length: - return [] - - # Handle special path queries - if query.startswith("/") or query.startswith("~"): - return self._handle_path_query(query) - - # Always search for files only since this is the files plugin - # triggered by "file" or "find" keywords - is_file_only = True - - # Use cached search for better performance - results = self._fast_search(query, file_only=is_file_only) - - # Sort by relevance and limit results - results.sort(key=lambda r: r.relevance, reverse=True) - return results[: self.max_results] - - def _warm_cache(self): - """Pre-warm cache for common directories in background.""" - - def warm_directory(path): - try: - if os.path.exists(path) and os.path.isdir(path): - self._get_cached_files(path) - except: - pass - - for path in self.search_paths: - threading.Thread(target=warm_directory, args=(path,), daemon=True).start() - - def _handle_path_query(self, query: str) -> List[Result]: - """Handle path-based queries like '/home' or '~/Documents'.""" - try: - expanded_path = ( - os.path.expanduser(query) if query.startswith("~") else query - ) - - if os.path.isdir(expanded_path): - return self._list_directory_contents(expanded_path) - - parent_dir = os.path.dirname(expanded_path) - if os.path.isdir(parent_dir): - basename = os.path.basename(expanded_path) - return self._search_in_directory(parent_dir, basename, max_results=10) - - except Exception: - pass - - return [] - - def _fast_search(self, query: str, file_only: bool = False) -> List[Result]: - """Fast search using cached file lists and optimized matching.""" - results = [] - query_lower = query.lower() - - # Early exit for empty queries - if len(query_lower) < self.min_query_length: - return results - - # Search in cached directories with early termination - for search_path in self.search_paths: - if not os.path.exists(search_path): - 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 - ): - continue - - cached_files = self._get_cached_files(search_path) - for file_path in cached_files: - if file_only and os.path.isdir(file_path): - continue - - filename = os.path.basename(file_path) - - # Fast pre-filter: check if query is in filename before expensive fuzzy matching - if query_lower not in filename.lower(): - # Only do fuzzy matching if simple substring match fails - if not self._simple_fuzzy_match(query_lower, filename): - continue - - relevance = self._calculate_relevance(filename, query_lower) - result = self._create_file_result(file_path, relevance) - if result: - results.append(result) - - # Early termination when we have enough results - if len(results) >= self.max_results * 3: - return results - - return results - - def _get_cached_files(self, directory: str) -> List[str]: - """Get cached file list for directory, refresh if needed.""" - current_time = time.time() - - with self.cache_lock: - if ( - directory in self.file_cache - and directory in self.cache_timestamp - and current_time - self.cache_timestamp[directory] < self.cache_ttl - ): - return self.file_cache[directory] - - files = self._scan_directory_fast(directory) - self.file_cache[directory] = files - self.cache_timestamp[directory] = current_time - - return files - - def _scan_directory_fast(self, directory: str) -> List[str]: - """Fast directory scan with optimized depth and early termination.""" - files = [] - current_depth = 0 - max_files_per_dir = 1000 # Limit files per directory to prevent memory issues - - def scan_dir(path: str, depth: int): - if depth > self.max_depth or len(files) >= max_files_per_dir: - return - - try: - with os.scandir(path) as entries: - for entry in entries: - try: - if entry.name.startswith("."): - continue - - # 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", - }: - continue - - files.append(entry.path) - - # Early termination if we have too many files - if len(files) >= max_files_per_dir: - return - - # Only recurse into directories if we're not at max depth - # and for home directory, be more selective - if entry.is_dir() and depth < self.max_depth: - if path == os.path.expanduser("~"): - # For home directory, only scan important subdirectories - if self._is_important_directory(entry.name): - scan_dir(entry.path, depth + 1) - else: - scan_dir(entry.path, depth + 1) - - except (PermissionError, OSError): - continue - except (PermissionError, OSError): - pass - - scan_dir(directory, current_depth) - return files - - def _is_important_directory(self, dirname: str) -> bool: - """Check if directory is important enough to scan deeper.""" - important_dirs = { - "Documents", - "Downloads", - "Desktop", - "Pictures", - "Videos", - "Music", - "Projects", - "Code", - "Development", - "Work", - "src", - "bin", - } - return dirname in important_dirs - - def _search_in_directory( - self, directory: str, query: str, max_results: int = 10 - ) -> List[Result]: - """Search for files in a specific directory.""" - results = [] - query_lower = query.lower() - - try: - with os.scandir(directory) as entries: - for entry in entries: - if self._simple_fuzzy_match(query_lower, entry.name): - relevance = self._calculate_relevance(entry.name, query_lower) - result = self._create_file_result(entry.path, relevance) - if result: - results.append(result) - - if len(results) >= max_results: - break - except (PermissionError, OSError): - pass - - return results - - def _list_directory_contents( - self, directory: str, max_results: int = 15 - ) -> List[Result]: - """List contents of a directory.""" - results = [] - - try: - with os.scandir(directory) as entries: - for entry in entries: - if not entry.name.startswith("."): - result = self._create_file_result(entry.path, 0.8) - if result: - results.append(result) - - if len(results) >= max_results: - break - except (PermissionError, OSError): - pass - - return results - - def _create_file_result(self, file_path: str, relevance: float) -> Result: - """Create a Result object for a file or directory.""" - try: - filename = os.path.basename(file_path) - is_dir = os.path.isdir(file_path) - - return Result( - title=filename, - subtitle=file_path, - icon_markup=icons.file, - action=lambda p=file_path: self._open_path(p), - relevance=relevance, - plugin_name=self.display_name, - data={"path": file_path, "type": "directory" if is_dir else "file"}, - ) - except Exception: - return None - - def _open_path(self, path: str): - """Open a file or directory with the default application.""" - try: - subprocess.Popen( - ["xdg-open", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - except Exception as e: - print(f"Failed to open {path}: {e}") - - def _open_file(self, file_path: str): - """Open a file with the default application.""" - self._open_path(file_path) - - def _open_directory(self, dir_path: str): - """Open a directory in the file manager.""" - self._open_path(dir_path) - - def _simple_fuzzy_match(self, query: str, text: str) -> bool: - """Optimized fuzzy matching with performance improvements.""" - text_lower = text.lower() - query_lower = query.lower() - - # Fast exact substring match - if query_lower in text_lower: - return True - - # Fast prefix match - if text_lower.startswith(query_lower): - return True - - # Only do expensive fuzzy matching for longer queries - 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"] - or fuzz.ratio(query_lower, text_lower) >= self.fuzzy_thresholds["ratio"] - ) - - return False - - def _calculate_relevance(self, filename: str, query: str) -> float: - """Calculate relevance score with optimized performance.""" - filename_lower = filename.lower() - query_lower = query.lower() - - # Fast exact match - if query_lower == filename_lower: - return 1.0 - - # Fast prefix match - if filename_lower.startswith(query_lower): - return 0.95 - - # Fast substring match - if query_lower in filename_lower: - # Calculate position-based relevance - position = filename_lower.index(query_lower) - position_score = 1.0 - (position / len(filename_lower)) - return 0.8 + (position_score * 0.1) - - # Only do expensive fuzzy matching for longer queries - if len(query_lower) >= 3: - partial = fuzz.partial_ratio(query_lower, filename_lower) - ratio = fuzz.ratio(query_lower, filename_lower) - - # Simplified relevance calculation - relevance = (partial * 0.6 + ratio * 0.4) / 100.0 - return max(relevance, 0.3) # Minimum relevance for fuzzy matches - - return 0.3 # Default relevance for short queries diff --git a/modules/launcher/plugins/archive/notifications.py b/modules/launcher/plugins/archive/notifications.py deleted file mode 100644 index 5e4df12..0000000 --- a/modules/launcher/plugins/archive/notifications.py +++ /dev/null @@ -1,611 +0,0 @@ -import time -from datetime import datetime -from typing import List, Optional - -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from gi.repository import GdkPixbuf - -import utils.icons as icons -from modules.launcher.plugin_base import PluginBase -from modules.launcher.result import Result -from services import notification_service - - -class NotificationDetailWidget(Box): - """Widget for displaying detailed notification content.""" - - def __init__(self, notification_data: dict, plugin_instance): - super().__init__(name="notification-detail", orientation="v", spacing=8) - - self.notification_data = notification_data - self.plugin = plugin_instance - - # Set size constraints to fit within launcher - self.set_size_request(530, 200) - self.set_vexpand(False) - self.set_hexpand(True) - - self.setup_ui() - - def setup_ui(self): - """Setup the detail view UI.""" - # Header with title and close button - header_box = Box(orientation="h", spacing=8) - - # Title - title_label = Label( - label=f"📧 {self.notification_data.get('app_name', 'Unknown')}", - name="notification-detail-title", - h_align="start", - h_expand=True, - ) - header_box.add(title_label) - - # Close button - close_btn = Button( - child=Label(markup=icons.cancel, name="notification-close-icon"), - name="notification-close-btn", - on_clicked=lambda *_: self.plugin._close_detail_view(), - ) - header_box.add(close_btn) - - self.add(header_box) - - # Content area - content_box = Box(orientation="v", spacing=4) - - # Summary - summary = self.notification_data.get("summary", "No title") - summary_label = Label( - label=f"Subject: {summary}", - name="notification-detail-summary", - h_align="start", - ellipsize="end", - ) - content_box.add(summary_label) - - # Body content - 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", - ) - # Set line wrapping using properties - body_label.set_property("wrap", True) - body_label.set_property("wrap-mode", 2) # WORD wrap mode - content_box.add(body_label) - - # Timestamp - 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" - ) - content_box.add(time_label) - - self.add(content_box) - - # Action buttons - actions_box = Box(orientation="h", spacing=8) - - # Remove button - remove_btn = Button( - child=Label( - markup=f"{icons.trash} Remove", name="notification-action-label" - ), - name="notification-action-btn", - on_clicked=lambda *_: self._remove_notification(), - ) - actions_box.add(remove_btn) - - self.add(actions_box) - - def _remove_notification(self): - """Remove this notification.""" - notif_id = self.notification_data.get("notification_id", 0) - self.plugin._clear_notification(notif_id) - - -class NotificationsPlugin(PluginBase): - def __init__(self): - super().__init__() - self.name = "notifications" - self.display_name = "Notification History" - self.description = "Search and manage notification history" - - # Settings - self.max_results = 15 - self.notification_service = notification_service - self.showing_detail_for = None # Track which notification is showing details - - def initialize(self): - """Initialize the plugin.""" - 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 - ) - - def cleanup(self): - """Cleanup the plugin.""" - # Disconnect from notification service signals - try: - self.notification_service.disconnect( - "notification_count", self._on_notification_count_changed - ) - except: - pass - - def _format_timestamp(self, timestamp: float) -> str: - """Format timestamp to human-readable string.""" - try: - dt = datetime.fromtimestamp(timestamp) - now = datetime.now() - - # If today, show time only - if dt.date() == now.date(): - return dt.strftime("%H:%M") - - # If this week, show day and time - days_diff = (now.date() - dt.date()).days - if days_diff < 7: - return dt.strftime("%a %H:%M") - - # Otherwise show date - return dt.strftime("%m/%d %H:%M") - except: - return "Unknown" - - def _get_notification_icon(self, notification) -> Optional[GdkPixbuf.Pixbuf]: - """Get icon for notification.""" - try: - # Try to use notification's image first - 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 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 - ) - except: - pass - - return None - except: - return None - - def _truncate_text(self, text: str, max_length: int = 60) -> str: - """Truncate text to specified length.""" - if not text: - return "" - if len(text) <= max_length: - return text - return text[: max_length - 3] + "..." - - def _trigger_refresh(self): - """Trigger launcher refresh to return to default notification view.""" - try: - from gi.repository import GLib - - def trigger_refresh(): - try: - # Try to access the launcher through the fabric Application - from fabric import Application - - app = Application.get_default() - - if app and hasattr(app, "launcher"): - launcher = app.launcher - if launcher and hasattr(launcher, "search_entry"): - # Get current search text to preserve the query - current_text = launcher.search_entry.get_text() - # Trigger the search to refresh results - if hasattr(launcher, "_perform_search"): - launcher._perform_search(current_text) - return False - - # 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" - ): - current_text = obj.search_entry.get_text() - obj._perform_search(current_text) - return False - - except Exception as e: - print(f"Error forcing launcher refresh: {e}") - - return False # Don't repeat - - # Use immediate refresh - GLib.timeout_add(10, trigger_refresh) - - except Exception as e: - print(f"Could not trigger refresh: {e}") - - def _clear_notification(self, notification_id: int): - """Remove a specific notification from history.""" - try: - # Debug: Print notification removal - print(f"[NotificationPlugin] Removing notification ID: {notification_id}") - - # Get count before removal for comparison - count_before = len(self.notification_service.all_notifications) - - # Remove the notification - self.notification_service.remove_notification(notification_id) - - # Get count after removal - count_after = len(self.notification_service.all_notifications) - - # Debug: Print removal result - print( - f"[NotificationPlugin] Notifications before: {count_before}, after: { - count_after - }" - ) - - if count_after < count_before: - print( - f"[NotificationPlugin] Successfully removed notification { - notification_id - }" - ) - else: - 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 - }" - ) - # Still try to refresh even if there was an error - self._trigger_refresh() - - def _clear_all_notifications(self): - """Clear all notifications from history.""" - self.notification_service.clear_all_notifications() - - def _toggle_dnd(self, enable: bool = None): - """Toggle or set DND mode.""" - try: - if enable is None: - # Toggle current state - new_state = not self.notification_service.dont_disturb - else: - # Set specific state - new_state = enable - - self.notification_service.dont_disturb = new_state - status = "enabled" if new_state else "disabled" - print(f"[NotificationPlugin] Do Not Disturb {status}") - - # Trigger refresh to show updated status - self._trigger_refresh() - - except Exception as e: - print(f"[NotificationPlugin] Error toggling DND: {e}") - - 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" - ) - self._trigger_refresh() - except Exception as e: - print(f"[NotificationPlugin] Error handling notification count change: {e}") - - def _get_notification_count(self) -> int: - """Get total number of notifications.""" - try: - return len(self.notification_service.all_notifications) - except: - return 0 - - def _show_notification_details(self, notification_data: dict): - """Show detailed view of a notification.""" - notif_id = notification_data.get("notification_id", 0) - self.showing_detail_for = notif_id - self._trigger_refresh() - - def _close_detail_view(self): - """Close the detail view and return to list.""" - self.showing_detail_for = None - self._trigger_refresh() - - def query(self, query_string: str) -> List[Result]: - """Search notification history.""" - results = [] - - # Check if we should show detail view for a specific notification - if self.showing_detail_for is not None: - 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) - - 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", ""), - } - - 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, - }, - ) - ) - return results - except Exception as e: - print(f"Error showing notification details: {e}") - self.showing_detail_for = None - - # 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, - ) - ) - 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}, - ) - ) - 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}, - ) - ) - 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 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 - ) - ) - - 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, - ) - ) - return results - - # Show count if no query - if not query_string: - count = len(notifications) - dnd_status = self.notification_service.dont_disturb - - # 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}, - ) - ) - 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=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 "" - filtered_notifications = [] - - 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()) - - # Search in summary, body, and app name - 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)) - - # Sort by timestamp (newest first) - filtered_notifications.sort(key=lambda x: x[2], reverse=True) - - # Limit results (leave room for header if no query) - max_notif_results = self.max_results - (1 if not query_string else 0) - filtered_notifications = filtered_notifications[:max_notif_results] - - # 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") - - # Create title and subtitle - title = self._truncate_text(summary, 50) - subtitle_parts = [] - - if body: - subtitle_parts.append(self._truncate_text(body, 40)) - - subtitle_parts.append(f"from {app_name}") - subtitle_parts.append(self._format_timestamp(timestamp)) - subtitle = " â€ĸ ".join(subtitle_parts) - - # Get icon - icon = self._get_notification_icon(notif) - - result = Result( - title=title, - subtitle=subtitle, - description="", # Don't show description by default - icon=icon, - 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), - 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, - }, - ) - 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, - ) - ) - - return results diff --git a/modules/launcher/plugins/archive/pomodoro.py b/modules/launcher/plugins/archive/pomodoro.py deleted file mode 100644 index 7c2a121..0000000 --- a/modules/launcher/plugins/archive/pomodoro.py +++ /dev/null @@ -1,422 +0,0 @@ -import subprocess -import threading -import time -from typing import List - -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: - """ - Core Pomodoro timer implementation. - """ - - def __init__(self): - self.is_break = True - self.timer = None - self.countdown_timers = [] # Store countdown timer references - self.remaining_till_long_break = 0 - self.end_time = 0 - self.pomodoro_duration = 0 - self.break_duration = 0 - self.long_break_duration = 0 - self.count = 0 - self.current_cycle = 0 - self.total_cycles_completed = 0 - self._cached_status = "Inactive" - self._last_status_update = 0 - - 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, - ], - check=False, - ) - except Exception as e: - print(f"Failed to send notification: {e}") - - # Run in background thread to avoid blocking UI - thread = threading.Thread(target=send_notification, daemon=True) - thread.start() - - def _send_countdown_notification_async(self, seconds_left): - """Send countdown notification asynchronously.""" - - def send_countdown(): - try: - subprocess.run( - [ - "notify-send", - "-a", - "Pomodoro Timer", - "-i", - "timer", - "-t", - "1000", # Show for 1 second - "⏰ Pomodoro Timer", - f"Time's up in {seconds_left} seconds!", - ], - check=False, - ) - except Exception as e: - print(f"Failed to send countdown notification: {e}") - - # Run in background thread to avoid blocking UI - thread = threading.Thread(target=send_countdown, daemon=True) - thread.start() - - def _schedule_countdown_notifications(self, duration_seconds): - """Schedule countdown notifications for the last 3 seconds.""" - # Clear any existing countdown timers - self._clear_countdown_timers() - - if duration_seconds > 3: - # Schedule countdown notifications for 3, 2, 1 seconds before end - for countdown in [3, 2, 1]: - delay = duration_seconds - countdown - if delay > 0: - timer = threading.Timer( - delay, self._send_countdown_notification_async, [countdown] - ) - self.countdown_timers.append(timer) - timer.start() - - def _clear_countdown_timers(self): - """Clear all countdown timers.""" - for timer in self.countdown_timers: - if timer.is_alive(): - timer.cancel() - self.countdown_timers.clear() - - def timeout(self): - """Handle timer timeout and switch between work/break periods.""" - if self.is_break: - # Starting work period - duration = self.pomodoro_duration * 60 - self.timer = threading.Timer(duration, self.timeout) - self.end_time = time.time() + duration - self.current_cycle += 1 - - # Send notification for work period start - self._send_notification_async( - "🍅 Work Time!", - f"Starting work period {self.current_cycle} ({ - self.pomodoro_duration - } minutes)", - ) - - # Schedule countdown notifications - self._schedule_countdown_notifications(duration) - - self.timer.start() - else: - # Starting break period - self.remaining_till_long_break -= 1 - self.total_cycles_completed += 1 - - if self.remaining_till_long_break == 0: - self.remaining_till_long_break = self.count - duration = self.long_break_duration * 60 - - # Send notification for long break - self._send_notification_async( - "🌟 Long Break Time!", - f"Take a long break ({ - self.long_break_duration - } minutes)\nYou've completed {self.count} cycles! Total: { - self.total_cycles_completed - }", - ) - - else: - duration = self.break_duration * 60 - - # Send notification for short break - self._send_notification_async( - "☕ Break Time!", - f"Take a short break ({self.break_duration} minutes)\nCycle { - self.current_cycle - } completed! Total: {self.total_cycles_completed}", - ) - - # Schedule countdown notifications for break end - self._schedule_countdown_notifications(duration) - - self.end_time = time.time() + duration - self.timer = threading.Timer(duration, self.timeout) - self.timer.start() - - self.is_break = not self.is_break - # Update cached status after state change - self._update_cached_status() - - def start(self, pomodoro_duration, break_duration, long_break_duration, count): - """Start the Pomodoro timer with specified durations.""" - self.stop() - self.pomodoro_duration = pomodoro_duration - self.break_duration = break_duration - self.long_break_duration = long_break_duration - self.count = count - self.remaining_till_long_break = count - self.current_cycle = 0 - self.total_cycles_completed = 0 - self.is_break = True - - # Send initial notification - self._send_notification_async( - "🍅 Pomodoro Started!", - f"Timer started: {pomodoro_duration}min work, {break_duration}min break\n{ - count - } cycles with {long_break_duration}min long breaks", - ) - - self.timeout() - - def stop(self): - """Stop the Pomodoro timer.""" - if self.is_active(): - self.timer.cancel() - self.timer = None - - # Clear countdown timers - self._clear_countdown_timers() - - # Send stop notification - self._send_notification_async( - "âšī¸ Pomodoro Stopped", - f"Timer stopped after { - self.total_cycles_completed - } completed cycles\nCurrent work period: {self.current_cycle}", - ) - - # Update cached status - self._cached_status = "Inactive" - - def is_active(self): - """Check if the timer is currently active.""" - return self.timer is not None - - def _update_cached_status(self): - """Update the cached status string.""" - current_time = time.time() - - if not self.is_active(): - self._cached_status = "Inactive" - return - - remaining_seconds = max(0, int(self.end_time - current_time)) - remaining_minutes = remaining_seconds // 60 - remaining_seconds = remaining_seconds % 60 - - if self.is_break: - if self.remaining_till_long_break == self.count: - period_type = "Long Break" - else: - period_type = "Short Break" - 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._last_status_update = current_time - - def get_status(self): - """Get current timer status with caching.""" - current_time = time.time() - - # Update cache if it's been more than 1 second or timer is inactive - if (current_time - self._last_status_update > 1.0) or not self.is_active(): - self._update_cached_status() - - return self._cached_status - - -class PomodoroPlugin(PluginBase): - """ - Pomodoro Technique plugin for the launcher. - """ - - # Default settings - DEFAULT_POMODORO_DURATION = 25 - DEFAULT_BREAK_DURATION = 5 - DEFAULT_LONG_BREAK_DURATION = 15 - DEFAULT_POMODORO_COUNT = 4 - - def __init__(self): - super().__init__() - self.display_name = "Pomodoro" - self.description = "Pomodoro Technique timer for productivity" - self.pomodoro = PomodoroTimer() - - def initialize(self): - """Initialize the Pomodoro plugin.""" - self.set_triggers(["pomo"]) - - def cleanup(self): - """Cleanup the Pomodoro plugin.""" - self.pomodoro.stop() - - 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 - 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" - ): - obj.search_entry.set_text("pomo ") - obj.search_entry.set_position(-1) - obj._perform_search("pomo ") - return False - - except Exception as e: - print(f"Error forcing launcher refresh: {e}") - - return False # Don't repeat - - # Use a small delay to ensure the action completes first - GLib.timeout_add(50, trigger_refresh) - - except Exception as e: - print(f"Could not trigger refresh: {e}") - - def query(self, query_string: str) -> List[Result]: - """Process Pomodoro queries.""" - results = [] - - if self.pomodoro.is_active(): - # Timer is running - show status and stop option - status = self.pomodoro.get_status() - - # Show current status - results.append( - Result( - title="Pomodoro Timer Active", - subtitle=status, - icon_markup=icons.timer_on, - action=lambda: None, # No action, just status display - relevance=1.0, - plugin_name=self.display_name, - data={"type": "status"}, - ) - ) - - # Show stop option with refresh - def stop_and_refresh(): - self.pomodoro.stop() - self._force_refresh() - - results.append( - Result( - title="Stop Pomodoro", - subtitle="Stop the current timer", - icon_markup=icons.timer_off, - action=stop_and_refresh, - relevance=0.9, - plugin_name=self.display_name, - data={"type": "stop"}, - ) - ) - else: - # Timer is not running - show start options - if not query_string.strip(): - # No parameters - show default start option - def start_default_and_refresh(): - self.pomodoro.start( - self.DEFAULT_POMODORO_DURATION, - self.DEFAULT_BREAK_DURATION, - self.DEFAULT_LONG_BREAK_DURATION, - self.DEFAULT_POMODORO_COUNT, - ) - self._force_refresh() - - results.append( - Result( - title="Start Pomodoro", - subtitle=f"{self.DEFAULT_POMODORO_DURATION} min work, {self.DEFAULT_BREAK_DURATION} min break, {self.DEFAULT_LONG_BREAK_DURATION} min long break, {self.DEFAULT_POMODORO_COUNT} cycles", - icon_markup=icons.timer_on, - action=start_default_and_refresh, - relevance=1.0, - plugin_name=self.display_name, - data={"type": "start_default"}, - ) - ) - else: - # Parse parameters - tokens = query_string.strip().split() - - # Validate parameters - if len(tokens) > 4 or not all(t.isdigit() for t in tokens): - results.append( - Result( - title="Invalid Parameters", - subtitle="Usage: [work_minutes] [break_minutes] [long_break_minutes] [cycle_count]", - icon_markup=icons.alert, - action=lambda: None, - relevance=0.5, - plugin_name=self.display_name, - data={"type": "error"}, - ) - ) - else: - # Parse valid parameters - p = ( - int(tokens[0]) - if len(tokens) > 0 - else self.DEFAULT_POMODORO_DURATION - ) - b = ( - int(tokens[1]) - if len(tokens) > 1 - else self.DEFAULT_BREAK_DURATION - ) - lb = ( - int(tokens[2]) - if len(tokens) > 2 - else self.DEFAULT_LONG_BREAK_DURATION - ) - c = ( - int(tokens[3]) - if len(tokens) > 3 - else self.DEFAULT_POMODORO_COUNT - ) - - def start_custom_and_refresh(_p=p, _b=b, _lb=lb, _c=c): - self.pomodoro.start(_p, _b, _lb, _c) - self._force_refresh() - - results.append( - Result( - title="Start Pomodoro", - subtitle=f"{p} min work, {b} min break, {lb} min long break, {c} cycles", - icon_markup=icons.timer_on, - action=start_custom_and_refresh, - relevance=1.0, - plugin_name=self.display_name, - data={"type": "start_custom", "params": [p, b, lb, c]}, - ) - ) - - return results diff --git a/modules/launcher/plugins/archive/screentime.py b/modules/launcher/plugins/archive/screentime.py deleted file mode 100644 index 9756d1e..0000000 --- a/modules/launcher/plugins/archive/screentime.py +++ /dev/null @@ -1,764 +0,0 @@ -import json -import os -import subprocess -import threading -import time -from datetime import datetime, timedelta -from typing import Dict, List, Optional -from pathlib import Path - -from gi.repository import GLib, Gtk, Gdk -from fabric.utils.helpers import get_relative_path -import utils.icons as icons -from utils.icon_resolver import IconResolver -from modules.launcher.plugin_base import PluginBase -from modules.launcher.result import Result - - -class ScreenTimeBarWidget(Gtk.Box): - """Custom widget to display screen time statistics like GNOME Digital Wellbeing.""" - - def __init__(self, usage_data: Dict[str, float], total_time: float, weekly_data: Dict[str, float] = None): - super().__init__( - name="screentime-widget", - orientation=Gtk.Orientation.VERTICAL, - spacing=12 - ) - - # Compact size for summary view only - self.set_size_request(540, 150) - self.set_vexpand(False) - self.set_hexpand(True) - - self.usage_data = usage_data - self.total_time = total_time - self.weekly_data = weekly_data or {} - - self.setup_ui() - - def setup_ui(self): - """Setup the GNOME-style digital wellbeing UI.""" - # Main container with minimal padding for launcher - main_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) - main_container.set_margin_left(12) - main_container.set_margin_right(12) - main_container.set_margin_top(0) - main_container.set_margin_bottom(0) - - # Header section with large time display - self.create_header_section(main_container) - - # Summary cards section - self.create_summary_cards(main_container) - - self.pack_start(main_container, True, True, 0) - self.show_all() - - def create_header_section(self, container): - """Create the main header with large time display.""" - header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) - - # Main title - title_label = Gtk.Label() - title_label.set_markup('Digital Wellbeing') - title_label.set_halign(Gtk.Align.CENTER) - title_label.get_style_context().add_class("screentime-title") - - # Large time display - total_hours = int(self.total_time // 3600) - total_minutes = int((self.total_time % 3600) // 60) - - time_display = Gtk.Label() - if total_hours > 0: - time_text = f"{total_hours}h {total_minutes}m" - else: - time_text = f"{total_minutes}m" - - time_display.set_markup(f'{time_text}') - time_display.set_halign(Gtk.Align.CENTER) - time_display.get_style_context().add_class("screentime-time-display") - - # Subtitle - subtitle = Gtk.Label() - subtitle.set_markup('Screen time today') - subtitle.set_halign(Gtk.Align.CENTER) - - header_box.pack_start(title_label, False, False, 0) - header_box.pack_start(time_display, False, False, 0) - header_box.pack_start(subtitle, False, False, 0) - - container.pack_start(header_box, False, False, 0) - - def create_summary_cards(self, container): - """Create summary statistics cards.""" - if not self.usage_data: - return - - cards_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - cards_box.set_homogeneous(True) - - # Most used app card - most_used_app = max(self.usage_data.items(), key=lambda x: x[1]) - most_used_hours = int(most_used_app[1] // 3600) - most_used_minutes = int((most_used_app[1] % 3600) // 60) - most_used_time = f"{most_used_hours}h {most_used_minutes}m" if most_used_hours > 0 else f"{most_used_minutes}m" - - most_used_card = self.create_info_card("Most Used", most_used_app[0], most_used_time) - - # App count card - app_count = len([app for app, time in self.usage_data.items() if time >= 60]) # Apps with 1+ minute - app_count_card = self.create_info_card("Apps Used", str(app_count), "applications") - - # Average session card (estimate) - if app_count > 0: - avg_session = self.total_time / app_count / 60 # minutes - avg_text = f"{int(avg_session)}m" - else: - avg_text = "0m" - avg_card = self.create_info_card("Avg Session", avg_text, "per app") - - cards_box.pack_start(most_used_card, True, True, 0) - cards_box.pack_start(app_count_card, True, True, 0) - cards_box.pack_start(avg_card, True, True, 0) - - container.pack_start(cards_box, False, False, 0) - - def create_info_card(self, title, value, subtitle): - """Create an information card widget.""" - card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - card.get_style_context().add_class("screentime-card") - card.set_margin_left(8) - card.set_margin_right(8) - card.set_margin_top(8) - card.set_margin_bottom(8) - - title_label = Gtk.Label() - title_label.set_markup(f'{title}') - title_label.set_halign(Gtk.Align.CENTER) - - value_label = Gtk.Label() - value_label.set_markup(f'{value}') - value_label.set_halign(Gtk.Align.CENTER) - value_label.set_ellipsize(3) # ELLIPSIZE_END - value_label.set_max_width_chars(12) - - subtitle_label = Gtk.Label() - subtitle_label.set_markup(f'{subtitle}') - subtitle_label.set_halign(Gtk.Align.CENTER) - - card.pack_start(title_label, False, False, 0) - card.pack_start(value_label, False, False, 0) - card.pack_start(subtitle_label, False, False, 0) - - return card - - def create_app_usage_section(self, container): - """Create the application usage section with grid layout.""" - if not self.usage_data: - no_data_label = Gtk.Label() - no_data_label.set_markup('No usage data available') - no_data_label.set_halign(Gtk.Align.CENTER) - no_data_label.set_margin_top(20) - container.pack_start(no_data_label, True, True, 0) - return - - # Section title - section_title = Gtk.Label() - section_title.set_markup('App Usage') - section_title.set_halign(Gtk.Align.START) - section_title.set_margin_top(8) - - # Scrollable area for app grid - scrolled = Gtk.ScrolledWindow() - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled.set_vexpand(True) - scrolled.set_min_content_height(120) - - # Container for app grid - self.grid_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - scrolled.add(self.grid_box) - - container.pack_start(section_title, False, False, 0) - container.pack_start(scrolled, True, True, 0) - - self.create_app_grid() - - def create_app_grid(self): - """Create grid layout for app usage display.""" - if not self.usage_data: - return - - # Sort apps by usage time (descending) - sorted_apps = sorted( - self.usage_data.items(), - key=lambda x: x[1], - reverse=True - ) - - # Show top 12 apps in grid format - max_apps = 12 - apps_to_show = [app for app in sorted_apps[:max_apps] if app[1] >= 60] # 1+ minute - - if not apps_to_show: - return - - # Create grid with 3 columns - cols = 3 - rows = (len(apps_to_show) + cols - 1) // cols # Ceiling division - - for row in range(rows): - row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) - row_box.set_homogeneous(True) - - for col in range(cols): - app_index = row * cols + col - if app_index < len(apps_to_show): - app_name, usage_time = apps_to_show[app_index] - app_card = self.create_app_grid_card(app_name, usage_time) - row_box.pack_start(app_card, True, True, 0) - else: - # Empty placeholder to maintain grid alignment - placeholder = Gtk.Box() - row_box.pack_start(placeholder, True, True, 0) - - self.grid_box.pack_start(row_box, False, False, 0) - - def create_app_grid_card(self, app_name: str, usage_time: float) -> Gtk.Box: - """Create a grid card for application usage display.""" - # Main card container - card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - card.get_style_context().add_class("screentime-grid-card") - card.set_margin_left(1) - card.set_margin_right(1) - card.set_margin_top(1) - card.set_margin_bottom(1) - - # Icon container - icon_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - icon_container.set_halign(Gtk.Align.CENTER) - - # Get real application icon (compact size for launcher) - icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 32) - if not icon_pixbuf: - # Fallback to default application icon - icon_pixbuf = self.icon_resolver.get_icon_pixbuf("application-x-executable", 32) - - if icon_pixbuf: - icon_image = Gtk.Image() - icon_image.set_from_pixbuf(icon_pixbuf) - icon_image.set_size_request(32, 32) - else: - # Final fallback to colored box - icon_image = Gtk.Box() - icon_image.set_size_request(32, 32) - icon_image.get_style_context().add_class("screentime-grid-icon") - - icon_container.pack_start(icon_image, False, False, 0) - - # App name - display_name = app_name if len(app_name) <= 10 else app_name[:7] + "..." - name_label = Gtk.Label() - name_label.set_markup(f'{display_name}') - name_label.set_halign(Gtk.Align.CENTER) - name_label.set_ellipsize(3) # ELLIPSIZE_END - name_label.set_max_width_chars(10) - - # Usage time - hours = int(usage_time // 3600) - minutes = int((usage_time % 3600) // 60) - time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m" - - time_label = Gtk.Label() - time_label.set_markup(f'{time_text}') - time_label.set_halign(Gtk.Align.CENTER) - - # Pack elements - card.pack_start(icon_container, False, False, 0) - card.pack_start(name_label, False, False, 0) - card.pack_start(time_label, False, False, 0) - - return card - - def create_modern_app_bar(self, app_name: str, usage_time: float, max_time: float) -> Gtk.Box: - """Create a modern GNOME-style application usage bar.""" - # Main container with rounded corners styling - container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - container.get_style_context().add_class("screentime-app-item") - container.set_margin_left(4) - container.set_margin_right(4) - container.set_margin_top(4) - container.set_margin_bottom(4) - - # Top row with app name and time - top_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - - # App icon and name - app_info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) - - # Get real application icon - icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 32) - if not icon_pixbuf: - # Fallback to default application icon - icon_pixbuf = self.icon_resolver.get_icon_pixbuf("application-x-executable", 32) - - if icon_pixbuf: - icon_image = Gtk.Image() - icon_image.set_from_pixbuf(icon_pixbuf) - icon_image.set_size_request(32, 32) - else: - # Final fallback to colored box if no icon found - icon_image = Gtk.Box() - icon_image.set_size_request(32, 32) - icon_image.get_style_context().add_class("screentime-app-icon") - - # App name with better typography - display_name = app_name if len(app_name) <= 20 else app_name[:17] + "..." - name_label = Gtk.Label() - name_label.set_markup(f'{display_name}') - name_label.set_halign(Gtk.Align.START) - name_label.set_ellipsize(3) # ELLIPSIZE_END - - app_info_box.pack_start(icon_image, False, False, 0) - app_info_box.pack_start(name_label, False, False, 0) - - # Time and percentage info - time_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) - time_info_box.set_halign(Gtk.Align.END) - - # Usage time - hours = int(usage_time // 3600) - minutes = int((usage_time % 3600) // 60) - time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m" - - time_label = Gtk.Label() - time_label.set_markup(f'{time_text}') - time_label.set_halign(Gtk.Align.END) - - # Percentage of total time - percentage = (usage_time / self.total_time * 100) if self.total_time > 0 else 0 - percentage_label = Gtk.Label() - percentage_label.set_markup(f'{percentage:.0f}%') - percentage_label.set_halign(Gtk.Align.END) - - time_info_box.pack_start(time_label, False, False, 0) - time_info_box.pack_start(percentage_label, False, False, 0) - - top_row.pack_start(app_info_box, False, False, 0) - top_row.pack_start(time_info_box, True, True, 0) - - # Modern progress bar with rounded corners - progress_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - progress_container.get_style_context().add_class("screentime-progress-container") - - progress_bar = Gtk.ProgressBar() - progress_bar.set_fraction(usage_time / max_time if max_time > 0 else 0) - progress_bar.set_show_text(False) - progress_bar.set_size_request(-1, 6) - progress_bar.get_style_context().add_class("screentime-progress") - - # Color based on usage intensity with modern colors - if percentage > 25: - progress_bar.get_style_context().add_class("high-usage") - elif percentage > 10: - progress_bar.get_style_context().add_class("medium-usage") - else: - progress_bar.get_style_context().add_class("low-usage") - - progress_container.pack_start(progress_bar, True, True, 0) - - container.pack_start(top_row, False, False, 0) - container.pack_start(progress_container, False, False, 0) - - return container - - -class ScreenTimePlugin(PluginBase): - """ - Screen time tracking plugin that monitors application usage. - """ - - def __init__(self): - super().__init__() - self.display_name = "Screen Time" - self.description = "Track application usage and screen time" - - # Data storage - single JSON file - self.data_file = get_relative_path("../../../config/json/screentime.json") - - # Icon resolver for application icons - self.icon_resolver = IconResolver() - - # Current tracking state - self.current_app = None - self.current_start_time = None - self.screentime_data = {"sessions": []} # Main data structure - - # Threading for monitoring - self.monitor_thread = None - self.stop_monitoring = threading.Event() - - # Last activity time for session tracking - self.last_activity_time = None - - def initialize(self): - """Initialize the screen time plugin.""" - self.set_triggers(["screentime", "st"]) - self.load_data() - self.start_monitoring() - - def cleanup(self): - """Cleanup the screen time plugin.""" - self.stop_monitoring.set() - if self.monitor_thread: - self.monitor_thread.join(timeout=2) - self.save_data() - - def load_data(self): - """Load screentime data from the JSON file.""" - try: - if os.path.exists(self.data_file): - with open(self.data_file, 'r') as f: - self.screentime_data = json.load(f) - if "sessions" not in self.screentime_data: - self.screentime_data["sessions"] = [] - else: - self.screentime_data = {"sessions": []} - - # Initialize today's session if it doesn't exist - today = datetime.now().strftime("%Y-%m-%d") - today_session = self.get_session_for_date(today) - if not today_session: - self.screentime_data["sessions"].append({ - "date": today, - "active_time": 0, - "apps": {} - }) - - except Exception as e: - print(f"ScreenTimePlugin: Error loading data: {e}") - self.screentime_data = {"sessions": []} - - def save_data(self): - """Save screentime data to the JSON file.""" - try: - # Ensure directory exists - os.makedirs(os.path.dirname(self.data_file), exist_ok=True) - - with open(self.data_file, 'w') as f: - json.dump(self.screentime_data, f, indent=2) - except Exception as e: - print(f"ScreenTimePlugin: Error saving data: {e}") - - def get_session_for_date(self, date_str: str) -> Optional[Dict]: - """Get session data for a specific date.""" - for session in self.screentime_data["sessions"]: - if session["date"] == date_str: - return session - return None - - def get_today_active_time(self) -> float: - """Get today's total active time in seconds.""" - today = datetime.now().strftime("%Y-%m-%d") - session = self.get_session_for_date(today) - return session["active_time"] if session else 0 - - def update_today_active_time(self, additional_seconds: float): - """Add time to today's active time.""" - today = datetime.now().strftime("%Y-%m-%d") - session = self.get_session_for_date(today) - - if session: - session["active_time"] += additional_seconds - else: - # Create new session for today - self.screentime_data["sessions"].append({ - "date": today, - "active_time": additional_seconds, - "apps": {} - }) - - def update_app_usage(self, app_name: str, additional_seconds: float): - """Add time to today's app usage.""" - today = datetime.now().strftime("%Y-%m-%d") - session = self.get_session_for_date(today) - - if session: - if "apps" not in session: - session["apps"] = {} - if app_name not in session["apps"]: - session["apps"][app_name] = 0 - session["apps"][app_name] += additional_seconds - else: - # Create new session for today - self.screentime_data["sessions"].append({ - "date": today, - "active_time": 0, - "apps": {app_name: additional_seconds} - }) - - def get_today_apps(self) -> Dict[str, float]: - """Get today's app usage data.""" - today = datetime.now().strftime("%Y-%m-%d") - session = self.get_session_for_date(today) - return session.get("apps", {}) if session else {} - - def get_weekly_total(self) -> float: - """Get total active time for the last 7 days.""" - today = datetime.now() - total_time = 0 - - for i in range(7): - date = (today - timedelta(days=i)).strftime("%Y-%m-%d") - session = self.get_session_for_date(date) - if session: - total_time += session["active_time"] - - return total_time - - def _get_yesterday_total(self) -> float: - """Get yesterday's total active time.""" - yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") - session = self.get_session_for_date(yesterday) - return session["active_time"] if session else 0 - - def start_monitoring(self): - """Start background monitoring of active windows.""" - if self.monitor_thread and self.monitor_thread.is_alive(): - return - - self.stop_monitoring.clear() - self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) - self.monitor_thread.start() - - def _monitor_loop(self): - """Main monitoring loop that tracks active time and app usage.""" - while not self.stop_monitoring.wait(2.0): # Check every 2 seconds - try: - current_time = time.time() - current_app = self._get_active_window_class() - - # If app changed, record time for previous app - if self.current_app and self.current_app != current_app: - if self.current_start_time: - elapsed = current_time - self.current_start_time - # Only count if elapsed time is reasonable (not more than 5 seconds) - if elapsed <= 5.0: - self.update_today_active_time(elapsed) - self.update_app_usage(self.current_app, elapsed) - - # Update current app tracking - if current_app: - self.current_app = current_app - self.current_start_time = current_time - else: - # No active window, reset tracking - self.current_app = None - self.current_start_time = None - - # Save data periodically (every 30 seconds) - if int(current_time) % 30 == 0: - self.save_data() - - except Exception as e: - print(f"ScreenTimePlugin: Error in monitor loop: {e}") - time.sleep(5) # Wait longer on error - - def _get_active_window_class(self) -> Optional[str]: - """Get the class name of the currently active window.""" - try: - result = subprocess.run( - ["hyprctl", "-j", "activewindow"], - capture_output=True, - text=True, - timeout=2 - ) - - if result.returncode == 0: - window_data = json.loads(result.stdout) - # Use class name, fallback to title if class is empty - app_class = window_data.get("class", "").strip() - if not app_class: - app_class = window_data.get("title", "Unknown").strip() - - # Filter out empty or system windows - if app_class and app_class not in ["", "Unknown", "Desktop"]: - return app_class - - except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as e: - print(f"ScreenTimePlugin: Error getting active window: {e}") - - return None - - def query(self, query_string: str) -> List[Result]: - """Process screen time queries with GNOME Digital Wellbeing style.""" - results = [] - query = query_string.strip().lower() - - # Get today's data - today_time = self.get_today_active_time() - today_apps = self.get_today_apps() - weekly_time = self.get_weekly_total() - - # Main screen time display (compact view) - if not query or query in ["today", "stats", "usage", "wellbeing"]: - today_hours = int(today_time // 3600) - today_minutes = int((today_time % 3600) // 60) - - # Enhanced subtitle with more context - if today_time > 0: - subtitle = f"Today: {today_hours}h {today_minutes}m" - - # Add comparison with yesterday if available - yesterday_total = self._get_yesterday_total() - if yesterday_total > 0: - if today_time > yesterday_total: - diff = today_time - yesterday_total - diff_minutes = int(diff // 60) - subtitle += f" (+{diff_minutes}m vs yesterday)" - elif today_time < yesterday_total: - diff = yesterday_total - today_time - diff_minutes = int(diff // 60) - subtitle += f" (-{diff_minutes}m vs yesterday)" - - # Add weekly average - weekly_avg = weekly_time / 7 if weekly_time > 0 else 0 - if weekly_avg > 0: - avg_hours = int(weekly_avg // 3600) - avg_minutes = int((weekly_avg % 3600) // 60) - avg_text = f"{avg_hours}h {avg_minutes}m" if avg_hours > 0 else f"{avg_minutes}m" - subtitle += f" â€ĸ Weekly avg: {avg_text}/day" - else: - subtitle = "No screen time recorded today" - - results.append( - Result( - title="Digital Wellbeing", - subtitle=subtitle, - icon_markup=icons.screentime, - action=lambda: None, - relevance=1.0, - plugin_name=self.display_name, - data={"type": "screentime_display"} - ) - ) - - # App list view (individual result items) - elif query in ["list", "apps", "applications"]: - if today_apps: - # Sort apps by usage time (descending) - sorted_apps = sorted( - today_apps.items(), - key=lambda x: x[1], - reverse=True - ) - - # Show top 12 apps as individual results - for i, (app_name, usage_time) in enumerate(sorted_apps[:12]): - if usage_time < 60: # Skip apps with less than 1 minute - continue - - hours = int(usage_time // 3600) - minutes = int((usage_time % 3600) // 60) - time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m" - - # Calculate percentage of total time - percentage = (usage_time / today_time * 100) if today_time > 0 else 0 - - subtitle = f"Used for {time_text} today ({percentage:.0f}%)" - - # Get application icon - app_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 48) - - results.append( - Result( - title=app_name, - subtitle=subtitle, - icon=app_icon_pixbuf, - icon_markup=icons.screentime if not app_icon_pixbuf else None, - action=lambda: None, - relevance=1.0 - (i * 0.1), # Decrease relevance for lower usage - plugin_name=self.display_name, - data={"type": "app_usage_item", "app": app_name} - ) - ) - else: - results.append( - Result( - title="No App Usage Data", - subtitle="Start using applications to see usage statistics", - icon_markup=icons.info, - action=lambda: None, - relevance=0.8, - plugin_name=self.display_name, - data={"type": "no_data"} - ) - ) - - # Weekly summary option - elif query in ["week", "weekly"]: - weekly_hours = int(weekly_time // 3600) - weekly_minutes = int((weekly_time % 3600) // 60) - daily_avg = weekly_time / 7 - avg_hours = int(daily_avg // 3600) - avg_minutes = int((daily_avg % 3600) // 60) - - results.append( - Result( - title="Weekly Screen Time", - subtitle=f"Total: {weekly_hours}h {weekly_minutes}m â€ĸ Daily avg: {avg_hours}h {avg_minutes}m", - icon_markup=icons.calendar, - action=lambda: None, - relevance=1.0, - plugin_name=self.display_name, - data={"type": "weekly_stats"} - ) - ) - - # Show help if no data - if not today_apps and not query: - results.append( - Result( - title="Digital Wellbeing", - subtitle="Start using applications to see your screen time statistics", - icon_markup=icons.info, - action=lambda: None, - relevance=0.8, - plugin_name=self.display_name, - data={"type": "help"} - ) - ) - - # Quick stats for specific app queries - if query and query not in ["today", "stats", "usage", "wellbeing", "week", "weekly"]: - matching_apps = [ - (app, time_spent) for app, time_spent in today_apps.items() - if query in app.lower() - ] - - for app_name, time_spent in matching_apps[:3]: - hours = int(time_spent // 3600) - minutes = int((time_spent % 3600) // 60) - percentage = (time_spent / today_time * 100) if today_time > 0 else 0 - time_text = f"{hours}h {minutes}m" if hours > 0 else f"{minutes}m" - - subtitle = f"Today: {time_text} ({percentage:.0f}%)" - - # Get application icon pixbuf - app_icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_name.lower(), 48) - - results.append( - Result( - title=f"{app_name}", - subtitle=subtitle, - icon=app_icon_pixbuf, - icon_markup=icons.screentime if not app_icon_pixbuf else None, - action=lambda: None, - relevance=0.9, - plugin_name=self.display_name, - data={"type": "app_stat", "app": app_name} - ) - ) - - return results - - diff --git a/styles/launcher.css b/styles/launcher.css index 1f7c86e..f115f2b 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -22,7 +22,6 @@ border-radius: 16px; } -/* Keyboard focus styling for header buttons */ #close-button.focused, #config-button.focused { background-color: var(--primary); @@ -175,189 +174,3 @@ border-radius: 8px; margin-bottom: 4px; } - -/* Launcher notification widget styling */ -#launcher-notification-wrapper { - margin: 4px 0; -} - -#launcher-notification-wrapper #notification-box { - margin: 0; - min-width: 500px; -} - -#launcher-notification-actions { - padding: 8px 16px; - background-color: var(--surface); - border-radius: 0 0 20px 20px; - margin-top: -4px; -} - -#launcher-notification-actions #notification-action-btn { - background-color: var(--surface-bright); - border-radius: 12px; - padding: 6px 12px; - transition: all 0.2s ease; -} - -#launcher-notification-actions #notification-action-btn:hover { - background-color: var(--red-dim); -} - -#launcher-notification-actions #notification-action-btn:hover #notification-action-label { - color: var(--background); -} - -#launcher-notification-actions #notification-action-label { - color: var(--red-dim); - font-size: 12px; - font-weight: 600; -} - -/* Screen Time Widget Styling - GNOME Digital Wellbeing Style */ -#screentime-widget { - background-color: var(--surface); - border-radius: 20px; - padding: 0; - margin: 8px 0; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -/* Title styling */ -.screentime-title { - color: var(--foreground); - margin-bottom: 8px; -} - -/* Large time display */ -.screentime-time-display { - margin: 12px 0; - font-family: "SF Pro Display", system-ui, sans-serif; - font-weight: 300; -} - -/* Summary cards */ -.screentime-card { - background-color: var(--surface-bright); - border-radius: 12px; - padding: 16px 12px; - border: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; -} - -.screentime-card:hover { - background-color: var(--surface-container); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -/* App item styling */ -.screentime-app-item { - background-color: var(--surface-bright); - border-radius: 12px; - padding: 12px 16px; - border: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; -} - -.screentime-app-item:hover { - background-color: var(--surface-container); -} - -/* Grid card styling - compact for launcher */ -.screentime-grid-card { - background-color: var(--surface-bright); - border-radius: 8px; - padding: 6px 4px; - border: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.2s ease; - min-width: 80px; - min-height: 70px; -} - -.screentime-grid-card:hover { - background-color: var(--surface-container); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -/* Grid icon styling */ -.screentime-grid-icon { - background: linear-gradient(135deg, var(--primary), var(--primary-bright)); - border-radius: 8px; - min-width: 32px; - min-height: 32px; -} - -/* App icon placeholder */ -.screentime-app-icon { - background: linear-gradient(135deg, var(--primary), var(--primary-bright)); - border-radius: 8px; - min-width: 32px; - min-height: 32px; -} - -/* Progress bar container */ -.screentime-progress-container { - background-color: rgba(255, 255, 255, 0.1); - border-radius: 3px; - padding: 0; -} - -/* Modern progress bars */ -.screentime-progress { - border-radius: 3px; - background-color: transparent; -} - -.screentime-progress trough { - background-color: rgba(255, 255, 255, 0.1); - border-radius: 3px; - min-height: 6px; -} - -.screentime-progress progress { - border-radius: 3px; - min-height: 6px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Modern usage intensity colors */ -.screentime-progress.high-usage progress { - background: linear-gradient(90deg, #ff6b6b, #ee5a52); -} - -.screentime-progress.medium-usage progress { - background: linear-gradient(90deg, #ffd93d, #ffb700); -} - -.screentime-progress.low-usage progress { - background: linear-gradient(90deg, #51cf66, #37b24d); -} - -/* Scrolled window styling */ -#screentime-widget scrolledwindow { - border-radius: 12px; - background-color: transparent; - border: none; -} - -#screentime-widget scrolledwindow scrollbar { - background-color: transparent; - border-radius: 6px; - margin: 2px; -} - -#screentime-widget scrolledwindow scrollbar slider { - background-color: rgba(255, 255, 255, 0.2); - border-radius: 6px; - min-width: 6px; - transition: all 0.2s ease; -} - -#screentime-widget scrolledwindow scrollbar slider:hover { - background-color: rgba(255, 255, 255, 0.3); -} - -/* Labels */ -#screentime-widget label { - color: var(--foreground); -} diff --git a/styles/osd.css b/styles/osd.css index 407af9e..90f015a 100644 --- a/styles/osd.css +++ b/styles/osd.css @@ -1,7 +1,7 @@ #osd { background: var(--shadow); padding: 12px 20px; - margin-bottom: 60px; + margin: 70px; } #osd scale { diff --git a/styles/switcher.css b/styles/switcher.css index 6c36ad7..8a33033 100644 --- a/styles/switcher.css +++ b/styles/switcher.css @@ -1,8 +1,4 @@ -/* #application-switcher { - background-color: var(--shadow); -} */ - -#application-switcher-container{ +#application-switcher-container { background: var(--shadow); }