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);
}