✨ feat(network): add network management plugin for WiFi and Ethernet connections
This commit is contained in:
12 files changed
+1109
-268
No files matched your search
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -96,6 +96,7 @@ class PluginManager:
|
||||
"kanban",
|
||||
"calendar",
|
||||
"reminders",
|
||||
"network",
|
||||
]
|
||||
|
||||
for plugin_name in default_plugins:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -179,7 +179,9 @@ timer_on: str = ""
|
||||
spy: str = ""
|
||||
calculator: str = ""
|
||||
file: str = ""
|
||||
|
||||
calendar: str = ""
|
||||
connect: str = ""
|
||||
disconnect: str = "ﴖ"
|
||||
|
||||
# Dice
|
||||
dice_1: str = ""
|
||||
|
||||
Reference in new issue
Block a user