✨ feat(network): add network management plugin for WiFi and Ethernet connections

This commit is contained in:
sankalp committed 2025-06-19 01:32:28 +05:45
1 parent 5bfc9f7631
commit 75b73d7f60
12 files changed
+1109 -268

No files matched your search

+9 -9
View File
@@ -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
+136 -104
View File
@@ -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} <search>",
"config_version": "1.0"
}
},
"settings": {
"max_examples_shown": 2,
"default_icon": "apps",
"fallback_example_template": "{trigger} <search>",
"config_version": "1.0"
}
}
+21 -8
View File
@@ -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()
+127 -30
View File
@@ -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
+1
View File
@@ -96,6 +96,7 @@ class PluginManager:
"kanban",
"calendar",
"reminders",
"network",
]
for plugin_name in default_plugins:
+1 -1
View File
@@ -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 <text>' to quickly add tasks."
def cleanup(self):
+786
View File
@@ -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
+1 -1
View File
@@ -598,4 +598,4 @@ class Ethernet(Service):
tx_bytes = int(tx_file.read().strip())
return rx_bytes, tx_bytes
except FileNotFoundError:
return None, None
return None, None
+12 -12
View File
@@ -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;
+12 -6
View File
@@ -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;
}
-96
View File
@@ -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;
}
+3 -1
View File
@@ -179,7 +179,9 @@ timer_on: str = "&#xf756;"
spy: str = "&#xf227;"
calculator: str = "&#xeb80;"
file: str = "&#xeaa4;"
calendar: str = "&#xea53;"
connect: str = "&#xeade;"
disconnect: str = "&#xfd16;"
# Dice
dice_1: str = "&#xf08b;"