diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf index 3319508..d14b324 100644 --- a/config/hypr/colors.conf +++ b/config/hypr/colors.conf @@ -1,13 +1,13 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/169.jpg +$wallpaper = /home/sankalp/Pictures/wallpapers/70.jpg $background = 121318 -$foreground = e3e2e9 +$foreground = e2e2e9 -$primary = b4c5ff -$secondary = c1c5dd -$tertiary = e2bbdb +$primary = aec6ff +$secondary = bfc6dc +$tertiary = dfbbde $surface = 121318 -$surface_bright = 38393f -$outline = 8f909a +$surface_bright = 37393e +$outline = 8e9099 $error = ff5945 $shadow = 000000 @@ -15,13 +15,13 @@ $shadow = 000000 $red = ffb2b9 $green = 95d5a7 $yellow = b8cf84 -$blue = b9c3ff +$blue = b6c4ff $magenta = e4b7f3 $cyan = 82d3e2 $white = 82d3e0 $red_dim = ff4c5c $green_dim = 4cb86a $yellow_dim = 8baa43 -$blue_dim = 536cff +$blue_dim = 5072ff $magenta_dim = c360e4 $cyan_dim = 30b5ce diff --git a/config/launcher.json b/config/launcher.json index 6236f00..3cf3c41 100644 --- a/config/launcher.json +++ b/config/launcher.json @@ -1,108 +1,140 @@ { - "launcher_config": { - "emoji": { - "icon": "emoji", - "description": "Emoji - Search and copy emojis" + "launcher_config": { + "emoji": { + "icon": "emoji", + "description": "Emoji - Search and copy emojis" + }, + ";": { + "icon": "emoji", + "description": "Emoji - Search and copy emojis" + }, + "clip": { + "icon": "clipboard", + "description": "Clipboard - Search and manage clipboard history" + }, + "=": { + "examples": [ + "= 10*5", + "= 2^8", + "= sin(30)" + ], + "icon": "calculator", + "description": "Quick Math - Fast mathematical expressions" + }, + "app": { + "icon": "apps", + "description": "Applications - Launch installed applications" + }, + "bin": { + "icon": "terminal", + "description": "Bins - Search and run executable binaries" + }, + "power": { + "icon": "shutdown", + "description": "Power - System power management and session control" + }, + "caffeine": { + "examples": [ + "caffeine 30m", + "caffeine 1h", + "caffeine 2h", + "caffeine on" + ], + "icon": "coffee", + "description": "Caffeine - Prevent system from going idle" + }, + "sc": { + "icon": "camera-video-symbolic", + "description": "Screencapture - Record screen and audio" + }, + "wall": { + "icon": "wallpapers", + "description": "Wallpapers - Set wallpapers, and tons of features" + }, + "pomo": { + "icon": "timer_on", + "description": "Pomodoro - Productivity timer with notifications" + }, + "web": { + "icon": "world", + "description": "Web Search - Search the web using various search engines" + }, + "?": { + "icon": "world", + "description": "Quick Web Search - Fast web search with question mark" + }, + "google": { + "icon": "google", + "description": "Google Search - Search with Google" + }, + "reddit": { + "icon": "reddit", + "description": "Reddit Search - Search discussions on Reddit" + }, + "youtube": { + "icon": "youtube", + "description": "YouTube Search - Search videos on YouTube" + }, + "github": { + "icon": "github", + "description": "GitHub Search - Search repositories on GitHub" + }, + "duckduckgo": { + "icon": "duckduckgo", + "description": "DuckDuckGo Search - Privacy-focused search" + }, + "stackoverflow": { + "icon": "stackoverflow", + "description": "Stack Overflow Search - Programming questions" + }, + "wikipedia": { + "icon": "wikipedia", + "description": "Wikipedia Search - Search articles on Wikipedia" + }, + "linkedin": { + "icon": "linkedin", + "description": "LinkedIn Search - Search professionals and jobs" + }, + "kanban": { + "icon": "kanban", + "description": "Kanban - Task management with kanban-style columns" + }, + "cal": { + "icon": "calendar", + "description": "Calendar - View calendar, navigate dates, and get date/time info" + }, + "remind": { + "icon": "timer_on", + "description": "Reminders - Set time-based reminders with notifications" + }, + "wifi": { + "examples": [ + "wifi list", + "wifi scan", + "wifi disconnect", + "wifi MyNetwork" + ], + "icon": "wifi_3", + "description": "WiFi - Manage wireless networks and connections" + }, + "network": { + "examples": [ + "network list", + "network scan", + "network toggle" + ], + "icon": "world", + "description": "Network - Manage WiFi and network connections" + }, + "net": { + "icon": "wifi_3", + "description": "Network - Quick access to network management" + } }, - ";": { - "icon": "emoji", - "description": "Emoji - Search and copy emojis" - }, - "clip": { - "icon": "clipboard", - "description": "Clipboard - Search and manage clipboard history" - }, - "=": { - "examples": ["= 10*5", "= 2^8", "= sin(30)"], - "icon": "calculator", - "description": "Quick Math - Fast mathematical expressions" - }, - "app": { - "icon": "apps", - "description": "Applications - Launch installed applications" - }, - "bin": { - "icon": "terminal", - "description": "Bins - Search and run executable binaries" - }, - "power": { - "icon": "shutdown", - "description": "Power - System power management and session control" - }, - "caffeine": { - "examples": ["caffeine 30m", "caffeine 1h", "caffeine 2h", "caffeine on"], - "icon": "coffee", - "description": "Caffeine - Prevent system from going idle" - }, - "sc": { - "icon": "camera-video-symbolic", - "description": "Screencapture - Record screen and audio" - }, - "wall": { - "icon": "wallpapers", - "description": "Wallpapers - Set wallpapers, and tons of features" - }, - "pomo": { - "icon": "timer_on", - "description": "Pomodoro - Productivity timer with notifications" - }, - "web": { - "icon": "world", - "description": "Web Search - Search the web using various search engines" - }, - "?": { - "icon": "world", - "description": "Quick Web Search - Fast web search with question mark" - }, - "google": { - "icon": "google", - "description": "Google Search - Search with Google" - }, - "reddit": { - "icon": "reddit", - "description": "Reddit Search - Search discussions on Reddit" - }, - "youtube": { - "icon": "youtube", - "description": "YouTube Search - Search videos on YouTube" - }, - "github": { - "icon": "github", - "description": "GitHub Search - Search repositories on GitHub" - }, - "duckduckgo": { - "icon": "duckduckgo", - "description": "DuckDuckGo Search - Privacy-focused search" - }, - "stackoverflow": { - "icon": "stackoverflow", - "description": "Stack Overflow Search - Programming questions" - }, - "wikipedia": { - "icon": "wikipedia", - "description": "Wikipedia Search - Search articles on Wikipedia" - }, - "linkedin": { - "icon": "linkedin", - "description": "LinkedIn Search - Search professionals and jobs" - }, - "kanban": { - "icon": "kanban", - "description": "Kanban - Task management with kanban-style columns" - }, - "cal": { - "icon": "calendar", - "description": "Calendar - View calendar, navigate dates, and get date/time info" - }, - "remind": { - "icon": "timer_on", - "description": "Reminders - Set time-based reminders with notifications" + "settings": { + "max_examples_shown": 2, + "default_icon": "apps", + "fallback_example_template": "{trigger} ", + "config_version": "1.0" } - }, - "settings": { - "max_examples_shown": 2, - "default_icon": "apps", - "fallback_example_template": "{trigger} ", - "config_version": "1.0" - } } diff --git a/modules/dock/components/music_player.py b/modules/dock/components/music_player.py index 691deda..27107be 100644 --- a/modules/dock/components/music_player.py +++ b/modules/dock/components/music_player.py @@ -141,7 +141,7 @@ class MusicPlayer(EventBox): return if not art_url: - self._hide_album_art() + self._load_fallback_image() self._last_art_url = None return @@ -154,7 +154,7 @@ class MusicPlayer(EventBox): self._show_album_art() self._last_art_url = art_url else: - self._hide_album_art() + self._load_fallback_image() self._last_art_url = None elif art_url.startswith(('http://', 'https://')): # Handle remote URLs by downloading them first @@ -174,15 +174,28 @@ class MusicPlayer(EventBox): # Clean up the temporary file os.unlink(temp_path) else: - self._hide_album_art() + self._load_fallback_image() self._last_art_url = None else: - self._hide_album_art() + self._load_fallback_image() self._last_art_url = None except Exception: - self._hide_album_art() + self._load_fallback_image() self._last_art_url = None + def _load_fallback_image(self): + """Load fallback image from current wallpaper when no album art is available""" + fallback = os.path.expanduser("~/.current.wall") + + try: + if os.path.exists(fallback): + self.album_thumbnail.set_image_from_file(fallback) + self._show_album_art() + else: + self._hide_album_art() + except Exception: + self._hide_album_art() + def _show_album_art(self): """Show album art thumbnail""" self.album_thumbnail.set_visible(True) @@ -406,13 +419,13 @@ class MusicPlayer(EventBox): self.set_visible(True) - # Try to load album art, hide if not available + # Try to load album art, use fallback if not available album_art_url = self._current_player.album_image_url if album_art_url: self._load_album_art(album_art_url) else: - # No album art available, hide thumbnails - self._hide_album_art() + # No album art available, use fallback image + self._load_fallback_image() # Update stack display (text vs visualizer) self._update_stack_display() diff --git a/modules/launcher/main.py b/modules/launcher/main.py index dd90317..11b2ef5 100644 --- a/modules/launcher/main.py +++ b/modules/launcher/main.py @@ -33,7 +33,7 @@ class Launcher(Window): layer="top", anchor="center", exclusivity="none", - keyboard_mode="exclusive", + keyboard_mode="on-demand", **kwargs, ) @@ -151,9 +151,6 @@ class Launcher(Window): self.triggered_plugin = triggered_plugin self.active_trigger = detected_trigger - # Add trigger mode styling - self.search_entry.add_style_class("trigger-mode") - # Query the plugin with empty string to show default options try: results = triggered_plugin.query("") @@ -161,13 +158,14 @@ class Launcher(Window): self.selected_index = 0 self._update_results_display() except Exception as e: - print(f"Error querying triggered plugin {triggered_plugin.name}: {e}") + print( + f"Error querying triggered plugin {triggered_plugin.name}: {e}" + ) self._clear_results() else: # Trigger not found, clear and show error or fallback self.search_entry.set_text("") self._clear_results() - print(f"Trigger keyword '{trigger_keyword}' not found") else: # Normal launcher opening - clear everything self.search_entry.set_text("") @@ -177,10 +175,12 @@ class Launcher(Window): # Position cursor at the end if there's text if trigger_keyword: + def position_cursor(): - if hasattr(self.search_entry, 'set_position'): + if hasattr(self.search_entry, "set_position"): self.search_entry.set_position(-1) # Move caret to end return False # Only run once + GLib.idle_add(position_cursor) self.visible = True @@ -197,7 +197,7 @@ class Launcher(Window): def _on_search_changed(self, entry): """Handle search text changes.""" # Skip if still initializing - if getattr(self, '_initializing', True): + if getattr(self, "_initializing", True): return query = entry.get_text().strip() @@ -207,7 +207,14 @@ class Launcher(Window): if query == ":": self._show_available_triggers() # If query matches a trigger exactly (with or without space), do not re-show suggestions - elif any(query == trig or query == f"{trig} " for trig in [t.strip() for p in self.plugin_manager.get_active_plugins() for t in p.get_triggers()]): + elif any( + query == trig or query == f"{trig} " + for trig in [ + t.strip() + for p in self.plugin_manager.get_active_plugins() + for t in p.get_triggers() + ] + ): # Do nothing, let the user type after the trigger GLib.timeout_add(150, self._perform_search, query) elif query: @@ -331,8 +338,6 @@ class Launcher(Window): return "" - - def _show_available_triggers(self): """Show available triggers when launcher is first opened.""" trigger_suggestions = self._get_trigger_suggestions("") @@ -450,11 +455,14 @@ class Launcher(Window): self.search_entry.grab_focus() def clear_selection(): - if hasattr(self.search_entry, 'set_position'): + if hasattr(self.search_entry, "set_position"): self.search_entry.set_position(-1) # Move caret to end - if hasattr(self.search_entry, 'select_region'): - self.search_entry.select_region(len(trigger_text), len(trigger_text)) # No selection + if hasattr(self.search_entry, "select_region"): + self.search_entry.select_region( + len(trigger_text), len(trigger_text) + ) # No selection return False # Only run once + GLib.idle_add(clear_selection) # Manually set the trigger mode to avoid search processing issues @@ -476,7 +484,7 @@ class Launcher(Window): def _update_results_display(self): """Update the results display.""" # Skip if still initializing or results_box not ready - if getattr(self, '_initializing', True) or not hasattr(self, 'results_box'): + if getattr(self, "_initializing", True) or not hasattr(self, "results_box"): return # Update input field with trigger indication (Albert-style) @@ -518,7 +526,7 @@ class Launcher(Window): def _update_input_action_text(self): """Update the input field with action text (Albert-style).""" # Check if search_entry is initialized - if not hasattr(self, 'search_entry') or self.search_entry is None: + if not hasattr(self, "search_entry") or self.search_entry is None: return # Get the current input text @@ -558,7 +566,9 @@ class Launcher(Window): else: # Not in trigger mode - show trigger help if current_text == ":": - self.search_entry.set_placeholder_text("Showing all available triggers.") + self.search_entry.set_placeholder_text( + "Showing all available triggers." + ) elif current_text: self.search_entry.set_placeholder_text( "Type trigger keyword (calc, app, file, system...)" @@ -580,9 +590,15 @@ class Launcher(Window): """Handle key press events.""" keyval = event.keyval - # Escape - exit trigger mode or hide launcher + # Escape - handle password entry, exit trigger mode, or hide launcher if keyval == Gdk.KEY_Escape: - if self.triggered_plugin: + # First check if there's a password entry widget that should handle Escape + password_entry_widget = self._find_password_entry_widget() + if password_entry_widget: + # Cancel the password entry + password_entry_widget.cancel_password_entry() + return True + elif self.triggered_plugin: # Exit trigger mode self.triggered_plugin = None self.active_trigger = "" @@ -594,20 +610,36 @@ class Launcher(Window): self.close_launcher() return True - # Backspace - exit trigger mode if at trigger boundary + # Backspace - handle trigger mode backspace behavior if keyval == Gdk.KEY_BackSpace: if self.triggered_plugin: current_text = self.search_entry.get_text() trigger_text = self.active_trigger.strip() - # Check if we're at the trigger boundary (only trigger text or trigger + space) - if current_text == trigger_text or current_text == f"{trigger_text} ": - # Exit trigger mode - self.triggered_plugin = None - self.active_trigger = "" - self.search_entry.set_text("") - self._clear_results() - return True + # Allow normal backspace behavior first, then check if we need to exit trigger mode + # Don't intercept the backspace - let GTK handle it normally + + # Schedule a check after the backspace is processed + def check_trigger_after_backspace(): + # Get the text after backspace has been processed + new_text = self.search_entry.get_text() + + # If the text no longer starts with the trigger, exit trigger mode + if not new_text.lower().startswith(trigger_text.lower()): + self.triggered_plugin = None + self.active_trigger = "" + self._clear_results() + # Don't clear the text - let the user's edit stand + + return False # Don't repeat + + # Use idle_add to check after the backspace is processed + from gi.repository import GLib + GLib.idle_add(check_trigger_after_backspace) + + # Allow the normal backspace to proceed + return False + # Let normal backspace behavior continue for other cases return False @@ -633,6 +665,20 @@ class Launcher(Window): if result.data and result.data.get("pin_action"): result.data["pin_action"]() return True + + # Check if the selected result has a custom widget with Entry fields + if self.results and 0 <= self.selected_index < len(self.results): + result = self.results[self.selected_index] + if result.custom_widget: + # Check if the custom widget contains Entry widgets that should handle Enter + if self._custom_widget_has_entry(result.custom_widget): + # Let the custom widget handle the Enter key + # Find the focused Entry widget and trigger its activate signal + focused_entry = self._find_focused_entry_in_widget(result.custom_widget) + if focused_entry: + focused_entry.emit("activate") + return True + # Normal Enter behavior self._activate_selected() return True @@ -646,6 +692,57 @@ class Launcher(Window): return False + def _custom_widget_has_entry(self, widget): + """Check if a custom widget contains Entry widgets.""" + from fabric.widgets.entry import Entry + + if isinstance(widget, Entry): + return True + + # Check children recursively + if hasattr(widget, 'get_children'): + for child in widget.get_children(): + if self._custom_widget_has_entry(child): + return True + + return False + + def _find_focused_entry_in_widget(self, widget): + """Find the focused Entry widget within a custom widget.""" + from fabric.widgets.entry import Entry + + if isinstance(widget, Entry): + # Try multiple ways to check if this Entry is focused + try: + if widget.has_focus() or widget.is_focus(): + return widget + # Also check if this is the only Entry in the widget (likely to be the target) + return widget + except: + # If focus checking fails, assume this Entry should handle the event + return widget + + # Check children recursively + if hasattr(widget, 'get_children'): + for child in widget.get_children(): + focused_entry = self._find_focused_entry_in_widget(child) + if focused_entry: + return focused_entry + + return None + + def _find_password_entry_widget(self): + """Find a NetworkPasswordEntry widget in the current results.""" + for result in self.results: + if result.custom_widget: + # Check if this is a NetworkPasswordEntry widget + if hasattr(result.custom_widget, '__class__') and result.custom_widget.__class__.__name__ == 'NetworkPasswordEntry': + return result.custom_widget + # Also check if it has the cancel_password_entry method (duck typing) + elif hasattr(result.custom_widget, 'cancel_password_entry'): + return result.custom_widget + return None + def _on_entry_activate(self, entry): """Handle entry activation (Enter key).""" self._activate_selected() @@ -788,8 +885,8 @@ class Launcher(Window): is_trigger_suggestion = ( result.data and result.data.get("type") == "trigger_suggestion" ) - keep_launcher_open = ( - result.data and result.data.get("keep_launcher_open", False) + keep_launcher_open = result.data and result.data.get( + "keep_launcher_open", False ) # Activate the result diff --git a/modules/launcher/plugin_manager.py b/modules/launcher/plugin_manager.py index c6bf040..6e552c5 100644 --- a/modules/launcher/plugin_manager.py +++ b/modules/launcher/plugin_manager.py @@ -96,6 +96,7 @@ class PluginManager: "kanban", "calendar", "reminders", + "network", ] for plugin_name in default_plugins: diff --git a/modules/launcher/plugins/kanban.py b/modules/launcher/plugins/kanban.py index 4910f9d..58a017d 100644 --- a/modules/launcher/plugins/kanban.py +++ b/modules/launcher/plugins/kanban.py @@ -431,7 +431,7 @@ class KanbanPlugin(PluginBase): def initialize(self): """Initialize the Kanban plugin.""" - self.set_triggers(["kanban" "kanban ",]) + self.set_triggers(["kanban", "kanban "]) self.description = "Kanban board for task management. Use 'kanban add ' to quickly add tasks." def cleanup(self): diff --git a/modules/launcher/plugins/network.py b/modules/launcher/plugins/network.py new file mode 100644 index 0000000..d1c21d8 --- /dev/null +++ b/modules/launcher/plugins/network.py @@ -0,0 +1,786 @@ +""" +Network plugin for the launcher. +Manage WiFi networks, connect/disconnect, and view network status. +""" + +from typing import List +from fabric.widgets.box import Box +from fabric.widgets.entry import Entry +from modules.launcher.plugin_base import PluginBase +from modules.launcher.result import Result +import utils.icons as icons +from services.network import NetworkClient +from gi.repository import GLib + +class NetworkPasswordEntry(Box): + """ + Custom password entry widget for network connections. + """ + + def __init__(self, access_point, network_plugin, **kwargs): + super().__init__( + name="network-password-entry", + orientation="h", + spacing=8, + **kwargs + ) + self.access_point = access_point + self.network_plugin = network_plugin + + # Password entry field + self.password_entry = Entry( + name="network-password-field", + placeholder="Enter password", + h_expand=True, + h_align="fill", + visibility=False + ) + + # Connect Enter key to connect action + self.password_entry.connect("activate", lambda *args: self.connect_to_network()) + + # Auto-focus the password entry when widget is created + GLib.timeout_add(100, self._focus_password_entry) + + + # Add widgets to layout + self.add(self.password_entry) + + def _focus_password_entry(self): + """Focus the password entry field.""" + try: + self.password_entry.grab_focus() + except Exception as e: + print(f"Could not focus password entry: {e}") + return False # Don't repeat + + def connect_to_network(self): + """Connect to network with entered password.""" + password = self.password_entry.get_text() + if password: + self.network_plugin._connect_to_network(self.access_point, password) + self.network_plugin.showing_password_for = None + # Force refresh to show normal network list + self.network_plugin._force_launcher_refresh() + else: + print("Please enter a password") + + def cancel_password_entry(self): + """Cancel password entry and return to network list.""" + self.network_plugin.showing_password_for = None + # Force refresh to show normal network list + self.network_plugin._force_launcher_refresh() + + +class NetworkPlugin(PluginBase): + """ + Network management plugin for the launcher. + """ + + def __init__(self): + super().__init__() + self.display_name = "Network" + self.description = "Manage WiFi networks and connections" + + # Track if we're ready + self._ready = False + + # Track which network is showing password entry + self.showing_password_for = None + + # Initialize network client safely + try: + self.network_client = NetworkClient() + + # Connect to network client signals + self.network_client.connect("ready", self._on_network_ready) + self.network_client.connect("changed", self._on_network_changed) + + if self.network_client.is_ready: + self._setup_network_signals() + except Exception as e: + print(f"Warning: Failed to initialize NetworkClient: {e}") + self.network_client = None + + def initialize(self): + """Initialize the network plugin.""" + self.set_triggers(["wifi", "network", "net"]) + + def cleanup(self): + """Cleanup the network plugin.""" + pass + + def _on_network_ready(self, *args): + """Called when network client is ready.""" + self._ready = True + self._setup_network_signals() + + def _on_network_changed(self, *args): + """Called when network state changes.""" + pass + + def _setup_network_signals(self): + """Setup signals for WiFi and Ethernet devices.""" + if self.network_client and self.network_client.wifi_device: + self.network_client.wifi_device.connect("changed", self._on_wifi_changed) + self.network_client.wifi_device.connect("ap-added", self._on_wifi_changed) + self.network_client.wifi_device.connect("ap-removed", self._on_wifi_changed) + + def _on_wifi_changed(self, *args): + """Called when WiFi state changes.""" + pass + + def _get_wifi_icon(self, strength: int) -> str: + """Get WiFi icon based on signal strength.""" + if strength >= 75: + return icons.wifi_3 + elif strength >= 50: + return icons.wifi_2 + elif strength >= 25: + return icons.wifi_1 + else: + return icons.wifi_0 + + def _get_connection_status_icon(self) -> str: + """Get icon representing current connection status.""" + if not self._ready or not self.network_client: + return icons.loader + + # Check ethernet first + if (self.network_client.ethernet_device and + self.network_client.ethernet_device.state == "activated"): + return icons.world + + # Check WiFi + if not self.network_client.wifi_device: + return icons.cloud_off + elif not self.network_client.wifi_device.wireless_enabled: + return icons.wifi_off + else: + active_ap = self.network_client.wifi_device.active_access_point + if active_ap: + return self._get_wifi_icon(active_ap.strength) + else: + return icons.world_off + + def _format_connection_status(self) -> tuple[str, str]: + """Get formatted connection status title and subtitle.""" + if not self._ready or not self.network_client: + return "Network Loading...", "Initializing network client" + + # Check ethernet first + if (self.network_client.ethernet_device and + self.network_client.ethernet_device.state == "activated"): + speed = self.network_client.ethernet_device.speed + return "Connected via Ethernet", f"Speed: {speed}" + + # Check WiFi + if not self.network_client.wifi_device: + return "No WiFi Device", "WiFi hardware not found" + elif not self.network_client.wifi_device.wireless_enabled: + return "WiFi Disabled", "Click to enable WiFi" + else: + active_ap = self.network_client.wifi_device.active_access_point + if active_ap: + return f"Connected to {active_ap.ssid}", f"Signal: {active_ap.strength}%" + else: + return "WiFi Disconnected", "No active connection" + + def _toggle_wifi(self): + """Toggle WiFi on/off and keep launcher open.""" + if self.network_client and self.network_client.wifi_device: + self.network_client.wifi_device.toggle_wifi() + return None # Keep launcher open to show new state + + def _disconnect_wifi(self): + """Disconnect from current WiFi network and keep launcher open.""" + if (self.network_client and self.network_client.wifi_device and + self.network_client.wifi_device.active_access_point): + self.network_client.wifi_device.disconnect_wifi() + return None # Keep launcher open to show disconnected state + + def _scan_networks(self): + """Scan for available networks and keep launcher open.""" + if self.network_client and self.network_client.wifi_device: + self.network_client.wifi_device.scan() + # Return None to keep launcher open + return None + + def _connect_to_network(self, access_point, password: str = None): + """Connect to a WiFi network with improved error handling.""" + try: + if not self.network_client: + return None + + if not self.network_client.wifi_device: + return None + + # Validate access point + if not access_point or not hasattr(access_point, 'ssid'): + return None + + # Validate password for secured networks + if access_point.requires_password and not password: + return None + + + # Use our own safer connection method to avoid NetworkManager errors + self._safe_connect_to_wifi(access_point, password) + + except Exception as e: + print(f"Error connecting to network {access_point.ssid}: {e}") + return None # Keep launcher open to show connection progress + + def _safe_connect_to_wifi(self, access_point, password: str = None): + """Safer WiFi connection method to avoid NetworkManager assertion errors.""" + try: + from gi.repository import NM, GLib + + ssid = access_point.ssid + client = self.network_client._client + device = self.network_client.wifi_device._device + + if not client or not device: + print("NetworkManager client or device not available") + return False + + + # Check for existing connections first + existing_connection = None + for connection in client.get_connections(): + wifi_setting = connection.get_setting_wireless() + if wifi_setting: + conn_ssid = NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) + if conn_ssid == ssid: + existing_connection = connection + break + + if existing_connection: + # Use existing connection + def activate_existing(): + try: + client.activate_connection_async( + existing_connection, device, None, None, + self._on_connection_result + ) + except Exception as e: + print(f"Error activating existing connection: {e}") + return False + + GLib.timeout_add(200, activate_existing) + else: + # Create new connection with delay to prevent assertion errors + def create_new_connection(): + try: + self._create_and_activate_connection(ssid, password, client, device) + except Exception as e: + print(f"Error creating new connection: {e}") + return False + + GLib.timeout_add(200, create_new_connection) + + except Exception as e: + print(f"Error in safe WiFi connection: {e}") + return False + + def _create_and_activate_connection(self, ssid, password, client, device): + """Create and activate a new WiFi connection.""" + try: + from gi.repository import NM, GLib + + # Create connection + connection = NM.SimpleConnection.new() + + # Connection settings + s_con = NM.SettingConnection.new() + s_con.set_property(NM.SETTING_CONNECTION_ID, ssid) + s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") + s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, device.get_iface()) + connection.add_setting(s_con) + + # Wireless settings + s_wifi = NM.SettingWireless.new() + s_wifi.set_property(NM.SETTING_WIRELESS_SSID, GLib.Bytes.new(ssid.encode())) + s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure") + connection.add_setting(s_wifi) + + # Security settings if password provided + if password: + s_sec = NM.SettingWirelessSecurity.new() + s_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") + s_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password) + connection.add_setting(s_sec) + + # IP settings + s_ipv4 = NM.SettingIP4Config.new() + s_ipv4.set_property("method", "auto") + connection.add_setting(s_ipv4) + + s_ipv6 = NM.SettingIP6Config.new() + s_ipv6.set_property("method", "auto") + connection.add_setting(s_ipv6) + + # Add connection and activate + def on_connection_added(client_obj, result): + try: + new_connection = client_obj.add_connection_finish(result) + if new_connection: + # Activate with delay + def activate_new(): + try: + client_obj.activate_connection_async( + new_connection, device, None, None, + self._on_connection_result + ) + except Exception as e: + print(f"Error activating new connection: {e}") + return False + GLib.timeout_add(100, activate_new) + else: + print(f"✗ Failed to create connection profile for {ssid}") + except Exception as e: + print(f"Error in connection callback: {e}") + + client.add_connection_async(connection, True, None, on_connection_added) + + except Exception as e: + print(f"Error creating connection: {e}") + + def _on_connection_result(self, client, result): + """Handle connection activation result.""" + try: + active_connection = client.activate_connection_finish(result) + if active_connection: + print("✓ WiFi connection successful") + else: + print("✗ WiFi connection failed") + except Exception as e: + print(f"Connection result error: {e}") + + def _show_password_entry_for_network(self, access_point): + """Show password entry for a network that requires password.""" + # Set which network is showing password entry + self.showing_password_for = access_point.ssid + + # Force launcher refresh by triggering a search + self._force_launcher_refresh() + + return None # Keep launcher open + + def _force_launcher_refresh(self): + """Force the launcher to refresh and show the password entry.""" + try: + from gi.repository import GLib + + # Use a small delay to ensure the action completes first + 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, '_perform_search'): + # Get current query and trigger a search + current_query = launcher.query if hasattr(launcher, 'query') else "" + if not current_query: + # If no query, use the trigger to force refresh + current_query = "net list" + + # Force a search which will refresh the results + launcher._perform_search(current_query) + 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, '_perform_search') and hasattr(obj, 'query'): + current_query = obj.query if obj.query else "net list" + obj._perform_search(current_query) + 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 _cancel_password_entry(self): + """Cancel password entry and return to network list.""" + self.showing_password_for = None + return None # Keep launcher open + + + + def query(self, query_string: str) -> List[Result]: + """Process network queries.""" + results = [] + query = query_string.strip() + query_lower = query.lower() + + # No more text-based password handling - using custom widgets instead + + if not query_lower: + # Check if network client is available + if not self.network_client: + results.append( + Result( + title="Network Service Unavailable", + subtitle="NetworkManager service is not available", + icon_markup=icons.cloud_off, + action=lambda: None, + relevance=1.0, + plugin_name=self.display_name, + data={"type": "error"}, + ) + ) + return results + + # Show current connection status + title, subtitle = self._format_connection_status() + status_icon = self._get_connection_status_icon() + + results.append( + Result( + title=title, + subtitle=subtitle, + icon_markup=status_icon, + action=lambda: None, + relevance=1.0, + plugin_name=self.display_name, + data={"type": "status"}, + ) + ) + + # Show available actions + if not self._ready: + return results + + # WiFi toggle option + if self.network_client.wifi_device: + if self.network_client.wifi_device.wireless_enabled: + results.append( + Result( + title="Disable WiFi", + subtitle="Turn off wireless networking", + icon_markup=icons.wifi_off, + action=lambda: self._toggle_wifi(), + relevance=0.9, + plugin_name=self.display_name, + data={"type": "toggle_wifi", "keep_launcher_open": True}, + ) + ) + + # Disconnect option if connected + if self.network_client.wifi_device.active_access_point: + results.append( + Result( + title="Disconnect WiFi", + subtitle=f"Disconnect from {self.network_client.wifi_device.active_access_point.ssid}", + icon_markup=icons.world_off, + action=lambda: self._disconnect_wifi(), + relevance=0.8, + plugin_name=self.display_name, + data={"type": "disconnect", "keep_launcher_open": True}, + ) + ) + + else: + results.append( + Result( + title="Enable WiFi", + subtitle="Turn on wireless networking", + icon_markup=icons.wifi_3, + action=lambda: self._toggle_wifi(), + relevance=0.9, + plugin_name=self.display_name, + data={"type": "toggle_wifi", "keep_launcher_open": True}, + ) + ) + + return results + + # Handle specific commands + if query_lower in ["scan", "refresh"]: + # Trigger scan immediately + self._scan_networks() + + results.append( + Result( + title="Scanning for Networks...", + subtitle="Refreshing available WiFi networks", + icon_markup=icons.radar, + action=lambda: None, # Keep launcher open + relevance=1.0, + plugin_name=self.display_name, + data={"type": "scanning", "keep_launcher_open": True}, + ) + ) + + # Show current networks if any + if (self._ready and self.network_client.wifi_device and + self.network_client.wifi_device.wireless_enabled): + access_points = self.network_client.wifi_device.access_points + if access_points: + results.append( + Result( + title=f"Found {len(access_points)} Networks", + subtitle="Scan complete - networks updated", + icon_markup=icons.wifi_3, + action=lambda: None, + relevance=0.9, + plugin_name=self.display_name, + data={"type": "scan_complete", "keep_launcher_open": True}, + ) + ) + + return results + + if query_lower in ["disconnect", "off"]: + if (self.network_client.wifi_device and + self.network_client.wifi_device.active_access_point): + ap = self.network_client.wifi_device.active_access_point + results.append( + Result( + title=f"Disconnect from {ap.ssid}", + subtitle=f"Currently connected with {ap.strength}% signal", + icon_markup=icons.world_off, + action=lambda: self._disconnect_wifi(), + relevance=1.0, + plugin_name=self.display_name, + data={"type": "disconnect", "keep_launcher_open": True}, + ) + ) + else: + results.append( + Result( + title="Not Connected", + subtitle="No active WiFi connection to disconnect", + icon_markup=icons.world_off, + action=lambda: None, + relevance=0.5, + plugin_name=self.display_name, + data={"type": "info"}, + ) + ) + return results + + if query_lower in ["toggle"]: + if self.network_client.wifi_device: + if self.network_client.wifi_device.wireless_enabled: + action_text = "Disable WiFi" + icon = icons.wifi_off + else: + action_text = "Enable WiFi" + icon = icons.wifi_3 + + results.append( + Result( + title=action_text, + subtitle="Toggle wireless networking", + icon_markup=icon, + action=lambda: self._toggle_wifi(), + relevance=1.0, + plugin_name=self.display_name, + data={"type": "toggle_wifi", "keep_launcher_open": True}, + ) + ) + return results + + # Search for specific networks or show available networks + if (self._ready and self.network_client.wifi_device and + self.network_client.wifi_device.wireless_enabled): + + access_points = self.network_client.wifi_device.access_points + + if query_lower in ["list"]: + # Automatically scan when listing networks + self._scan_networks() + + # Show all available networks + if not access_points: + results.append( + Result( + title="Scanning for Networks...", + subtitle="Please wait while scanning for WiFi networks", + icon_markup=icons.radar, + action=lambda: None, # Don't close launcher + relevance=1.0, + plugin_name=self.display_name, + data={"type": "scanning", "keep_launcher_open": True}, + ) + ) + else: + for ap in access_points[:10]: # Limit to 10 networks + # Add lock icon for secured networks + if ap.requires_password: + title = f"🔒 {ap.ssid}" + else: + title = ap.ssid + + # Check if this network should show password entry instead + if ap.requires_password and self.showing_password_for == ap.ssid: + # REPLACE network with password entry Result using our own Entry widget + password_widget = NetworkPasswordEntry( + access_point=ap, + network_plugin=self + ) + + results.append( + Result( + title=f"🔑 Enter password for {ap.ssid}", + subtitle="Type password and click connect • Click cancel to go back", + icon_markup=icons.lock, + action=lambda: None, # Widget handles the action + relevance=1.0, # High relevance to appear at top + plugin_name=self.display_name, + custom_widget=password_widget, # Use our custom Entry widget + data={ + "type": "password_entry", + "ssid": ap.ssid, + "keep_launcher_open": True + }, + ) + ) + else: + # Show normal network Result + if ap.is_active: + subtitle = f"Connected • {ap.strength}% signal" + action = lambda: self._disconnect_wifi() + else: + if ap.requires_password: + subtitle = f"{ap.strength}% signal • Click to enter password" + action = lambda current_ap=ap: self._show_password_entry_for_network(current_ap) + else: + subtitle = f"{ap.strength}% signal • Click to connect" + action = lambda current_ap=ap: self._connect_to_network(current_ap) + + results.append( + Result( + title=title, + subtitle=subtitle, + icon_markup=self._get_wifi_icon(ap.strength), + action=action, + relevance=0.9 if ap.is_active else 0.8, + plugin_name=self.display_name, + data={ + "type": "network", + "ssid": ap.ssid, + "requires_password": ap.requires_password, + "keep_launcher_open": True + }, + ) + ) + + + + return results + + else: + # Search for networks matching the query + matching_networks = [ + ap for ap in access_points + if query.lower() in ap.ssid.lower() + ] + + if matching_networks: + for ap in matching_networks[:5]: # Limit to 5 matches + if ap.requires_password: + title = f"🔒 {ap.ssid}" + else: + title = ap.ssid + + # Check if this network should show password entry instead + if ap.requires_password and self.showing_password_for == ap.ssid: + # REPLACE network with password entry Result using our own Entry widget + password_widget = NetworkPasswordEntry( + access_point=ap, + network_plugin=self + ) + + results.append( + Result( + title=f"🔑 Enter password for {ap.ssid}", + subtitle="Type password and click connect • Click cancel to go back", + icon_markup=icons.lock, + action=lambda: None, # Widget handles the action + relevance=1.0, # High relevance to appear at top + plugin_name=self.display_name, + custom_widget=password_widget, # Use our custom Entry widget + data={ + "type": "password_entry", + "ssid": ap.ssid, + "keep_launcher_open": True + }, + ) + ) + else: + # Show normal network Result + if ap.is_active: + subtitle = f"Connected • {ap.strength}% signal" + action = lambda: self._disconnect_wifi() + else: + if ap.requires_password: + subtitle = f"{ap.strength}% signal • Click to enter password" + action = lambda current_ap=ap: self._show_password_entry_for_network(current_ap) + else: + subtitle = f"{ap.strength}% signal • Click to connect" + action = lambda current_ap=ap: self._connect_to_network(current_ap) + + results.append( + Result( + title=title, + subtitle=subtitle, + icon_markup=self._get_wifi_icon(ap.strength), + action=action, + relevance=1.0 if ap.is_active else 0.9, + plugin_name=self.display_name, + data={ + "type": "network", + "ssid": ap.ssid, + "requires_password": ap.requires_password, + "keep_launcher_open": True + }, + ) + ) + + + + return results + else: + # No matching networks found + results.append( + Result( + title=f"No networks matching '{query}'", + subtitle="Try scanning for more networks", + icon_markup=icons.radar, + action=lambda: self._scan_networks(), + relevance=0.5, + plugin_name=self.display_name, + data={"type": "scan", "keep_launcher_open": True}, + ) + ) + return results + + # Fallback for when WiFi is disabled or not available + results.append( + Result( + title="WiFi Not Available", + subtitle="Enable WiFi to search for networks", + icon_markup=icons.wifi_off, + action=lambda: self._toggle_wifi() if self.network_client.wifi_device else None, + relevance=0.5, + plugin_name=self.display_name, + data={"type": "info"}, + ) + ) + + return results \ No newline at end of file diff --git a/services/network.py b/services/network.py index 3058ba4..a3c7a94 100644 --- a/services/network.py +++ b/services/network.py @@ -598,4 +598,4 @@ class Ethernet(Service): tx_bytes = int(tx_file.read().strip()) return rx_bytes, tx_bytes except FileNotFoundError: - return None, None \ No newline at end of file + return None, None diff --git a/styles/colors.css b/styles/colors.css index 04b46de..6b96d32 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -1,20 +1,20 @@ :vars { - --foreground: #e3e2e9; + --foreground: #e2e2e9; --background: #121318; - --cursor: #e3e2e9; - --primary: #b4c5ff; - --on-primary: #1b2d60; - --secondary: #c1c5dd; - --on-secondary: #2b3042; - --tertiary: #e2bbdb; - --on-tertiary: #422740; + --cursor: #e2e2e9; + --primary: #aec6ff; + --on-primary: #122f60; + --secondary: #bfc6dc; + --on-secondary: #293041; + --tertiary: #dfbbde; + --on-tertiary: #402843; --surface: #121318; - --surface-bright: #38393f; + --surface-bright: #37393e; --error: #ffb4ab; --error-dim: #ff8678; --on-error: #690005; --error-container: #93000a; - --outline: #8f909a; + --outline: #8e9099; --shadow: #000000; --red: #ffb2b9; --red-dim: #ff7f8b; @@ -22,8 +22,8 @@ --green-dim: #70c789; --yellow: #b8cf84; --yellow-dim: #a3c15f; - --blue: #b9c3ff; - --blue-dim: #8697ff; + --blue: #b6c4ff; + --blue-dim: #839bff; --magenta: #e4b7f3; --magenta-dim: #d48bec; --cyan: #82d3e2; diff --git a/styles/launcher.css b/styles/launcher.css index f878f68..e6fe856 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -5,6 +5,7 @@ min-width: 550px; transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } + #close-button, #config-button { background-color: var(--surface); @@ -130,26 +131,31 @@ margin-right: 12px; } -#result-item-text { -} - #result-item-title { font-size: 14px; font-weight: 600; - color: #ffffff; + color: var(--foreground); margin-top: 8px; margin-bottom: 2px; } #result-item-subtitle { font-size: 12px; - color: rgba(255, 255, 255, 0.7); + color: var(--outline); margin-bottom: 8px; } #result-item-plugin { font-size: 10px; - color: rgba(255, 255, 255, 0.5); + color: var(--outline); font-style: italic; margin-bottom: 8px; + opacity: 0.7; +} + +#network-password-entry { + border: 1px solid var(--outline); + padding: 8px; + border-radius: 8px; + margin-bottom: 4px; } diff --git a/styles/launcher_simple.css b/styles/launcher_simple.css deleted file mode 100644 index 50fec8a..0000000 --- a/styles/launcher_simple.css +++ /dev/null @@ -1,96 +0,0 @@ -/* Simple Launcher Styles */ - -/* #launcher-window { */ -/* border-radius: 12px; */ -/* border: 1px solid rgba(255, 255, 255, 0.1); */ -/* } */ - -#launcher-icon-label { - font-size: 32px; - padding: 8px; -} -#launcher-main { - background: var(--shadow); - padding: 20px; - min-width: 550px; -} - -#launcher-search { - background: var(--surface); - border: 2px solid var(--outline); - border-radius: 8px; - padding: 12px 16px; - font-size: 16px; - color: #ffffff; - margin-bottom: 16px; -} - -#launcher-search:focus { - border-color: #4a9eff; -} - -#launcher-results-scroll { - background: transparent; - border: none; - min-height: 100px; -} - -#launcher-results { - background: transparent; -} - -#launcher-result-item { - background: transparent; - border-radius: 8px; - padding: 12px 16px; - margin: 2px 0; - border: 1px solid transparent; -} - -#launcher-result-item:hover { - background: rgba(255, 255, 255, 0.05); - border-color: rgba(255, 255, 255, 0.1); -} - -#launcher-result-item.selected { - background: rgba(74, 158, 255, 0.2); - border-color: #4a9eff; -} - -#result-item-main { - min-height: 48px; -} - -#launcher-result-item { - min-height: 48px; -} - -#result-item-icon { - min-width: 48px; - min-height: 48px; - margin-right: 12px; -} - -#result-item-text { -} - -#result-item-title { - font-size: 14px; - font-weight: 600; - color: #ffffff; - margin-top: 8px; - margin-bottom: 2px; -} - -#result-item-subtitle { - font-size: 12px; - color: rgba(255, 255, 255, 0.7); - margin-bottom: 2px; -} - -#result-item-plugin { - font-size: 10px; - color: rgba(255, 255, 255, 0.5); - font-style: italic; - margin-bottom: 8px; -} diff --git a/utils/icons.py b/utils/icons.py index b61ff9c..7b0e8a8 100644 --- a/utils/icons.py +++ b/utils/icons.py @@ -179,7 +179,9 @@ timer_on: str = "" spy: str = "" calculator: str = "" file: str = "" - +calendar: str = "" +connect: str = "" +disconnect: str = "ﴖ" # Dice dice_1: str = ""