diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf index 0d9926b..e5ce497 100644 --- a/config/hypr/colors.conf +++ b/config/hypr/colors.conf @@ -1,27 +1,27 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/example-1.jpg -$background = 1a1110 -$foreground = f1dedd +$wallpaper = /home/sankalp/Pictures/wallpapers/2.png +$background = 131318 +$foreground = e4e1e9 -$primary = ffb3ad -$secondary = e7bdb9 -$tertiary = e1c28c -$surface = 1a1110 -$surface_bright = 423736 -$outline = a08c8a +$primary = bcc2ff +$secondary = c4c5dd +$tertiary = e6bad6 +$surface = 131318 +$surface_bright = 39393f +$outline = 90909a $error = ff5945 $shadow = 000000 -$red = ffb4aa -$green = b7d085 -$yellow = dec56e -$blue = cebdfe -$magenta = fcb0d5 -$cyan = 84d5c4 +$red = ffb2b9 +$green = 95d5a7 +$yellow = b8cf84 +$blue = bdc2ff +$magenta = e4b7f3 +$cyan = 82d3e2 $white = 82d3e0 -$red_dim = ff5a44 -$green_dim = 89ad42 -$yellow_dim = bb9b2b -$blue_dim = 8359fc -$magenta_dim = f84ea1 -$cyan_dim = 3eb59c +$red_dim = ff4c5c +$green_dim = 4cb86a +$yellow_dim = 8baa43 +$blue_dim = 5764ff +$magenta_dim = c360e4 +$cyan_dim = 30b5ce diff --git a/config/settings/bluetooth_tab.py b/config/settings/bluetooth_tab.py new file mode 100644 index 0000000..7ed0aa9 --- /dev/null +++ b/config/settings/bluetooth_tab.py @@ -0,0 +1,418 @@ +from fabric.bluetooth import BluetoothClient, BluetoothDevice +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.image import Image +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import GLib, Gtk +import subprocess + +import utils.icons as icons + + +class BluetoothDeviceSlot(CenterBox): + def __init__(self, device: BluetoothDevice, **kwargs): + super().__init__(name="bluetooth-device", **kwargs) + self.device = device + self.device.connect("changed", self.on_changed) + self.device.connect( + "notify::closed", lambda *_: self.device.closed and self.destroy() + ) + + self.connection_label = Label(markup=icons.bluetooth_disconnected, style="font-size: 22px;") + self.connect_button = Button( + label="Connect", + on_clicked=lambda *_: self._handle_device_action(), + ) + + # Create device name label that we can update + self.device_name_label = Label( + label=device.alias or device.name, + h_expand=True, + h_align="start", + ellipsization="end" + ) + + self.start_children = [ + Box( + spacing=10, + h_expand=True, + h_align="fill", + children=[ + Image(icon_name=device.icon_name + "-symbolic", size=16), + self.device_name_label, + self.connection_label, + ], + ) + ] + self.end_children = self.connect_button + + self.device.emit("changed") + + + + def _handle_device_action(self): + """Handle connect/disconnect button click""" + if self.device.connected: + # Disconnect + self.device.connected = False + elif self.device.paired: + # Connect to already paired device + self.device.connected = True + else: + # Pair new device using bluetoothctl + self._pair_device_with_bluetoothctl() + + def _pair_device_with_bluetoothctl(self): + """Pair device using bluetoothctl command""" + try: + device_name = self.device.alias or self.device.name + device_address = self.device.address + + # Show pairing notification + subprocess.run([ + "notify-send", + "Bluetooth Pairing", + f"Attempting to pair with {device_name}...", + "--icon=bluetooth", + "--urgency=normal", + "--app-name=Bluetooth" + ], check=False) + + # Use bluetoothctl to pair the device + # This will trigger the system's pairing agent which should show the pairing dialog + pairing_process = subprocess.Popen([ + "bluetoothctl", "pair", device_address + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Monitor the pairing process in a separate thread + GLib.timeout_add(100, self._monitor_pairing_process, pairing_process, device_name) + + except Exception as e: + print(f"Failed to start pairing process: {e}") + self._show_pairing_failed_notification() + + def _monitor_pairing_process(self, process, device_name): + """Monitor the bluetoothctl pairing process""" + if process.poll() is None: + # Process still running + return True + + # Process finished, check result + stdout, stderr = process.communicate() + return_code = process.returncode + + if return_code == 0 and "Pairing successful" in stdout: + self._show_pairing_success_notification() + # Auto-trust and connect the device after successful pairing + self._trust_and_connect_device() + else: + self._show_pairing_failed_notification() + + return False # Stop monitoring + + def _trust_and_connect_device(self): + """Trust and connect to the device after successful pairing""" + try: + device_address = self.device.address + + # Trust the device + subprocess.run(["bluetoothctl", "trust", device_address], check=False) + + # Connect to the device + GLib.timeout_add(1000, lambda: setattr(self.device, 'connected', True)) + + except Exception as e: + print(f"Failed to trust/connect device: {e}") + + def _show_pairing_success_notification(self): + """Show notification when pairing succeeds""" + try: + device_name = self.device.alias or self.device.name + subprocess.run([ + "notify-send", + "Bluetooth Paired", + f"Successfully paired with {device_name}", + "--icon=bluetooth", + "--urgency=normal", + "--app-name=Bluetooth" + ], check=False) + except Exception as e: + print(f"Failed to send pairing success notification: {e}") + + def _show_pairing_failed_notification(self): + """Show notification when pairing fails""" + try: + device_name = self.device.alias or self.device.name + subprocess.run([ + "notify-send", + "Bluetooth Pairing Failed", + f"Failed to pair with {device_name}", + "--icon=bluetooth-disabled", + "--urgency=normal", + "--app-name=Bluetooth" + ], check=False) + except Exception as e: + print(f"Failed to send pairing failed notification: {e}") + + def on_changed(self, *_): + # Update connection icon + self.connection_label.set_markup( + icons.bluetooth_connected if self.device.connected else icons.bluetooth_disconnected + ) + + # Check for pairing state changes and show notifications + base_name = self.device.alias or self.device.name + + # Track pairing success/failure + if hasattr(self, '_was_pairing') and self._was_pairing: + if self.device.paired and not self.device.connecting: + # Pairing succeeded + self._show_pairing_success_notification() + self._was_pairing = False + elif not self.device.connecting and not self.device.paired: + # Pairing failed + self._show_pairing_failed_notification() + self._was_pairing = False + + # Update device name with status + if self.device.connecting: + if self.device.paired: + status_text = f"{base_name}\nConnecting..." + self.connect_button.set_label("Connecting...") + else: + status_text = f"{base_name}\nPairing..." + self.connect_button.set_label("Pairing...") + self._was_pairing = True # Track that we're pairing + elif self.device.connected: + battery_info = "" + if self.device.battery_percentage > 0: + battery_info = f" • {self.device.battery_percentage:.0f}%" + status_text = f"{base_name}\nConnected{battery_info}" + self.connect_button.set_label("Disconnect") + else: + status_text = base_name + if self.device.paired: + self.connect_button.set_label("Connect") + else: + self.connect_button.set_label("Pair") + + self.device_name_label.set_label(status_text) + + + +class BluetoothTab: + """Bluetooth device management tab for settings""" + + def __init__(self): + self.bluetooth_client = BluetoothClient(on_device_added=self.on_device_added) + self.device_widgets = {} + + # Connect to bluetooth client signals + self.bluetooth_client.connect("notify::enabled", self._on_bluetooth_changed) + self.bluetooth_client.connect("notify::scanning", self._update_scan_button) + self.bluetooth_client.connect("device-removed", self.on_device_removed) + + + + def create_bluetooth_tab(self): + """Create the Bluetooth tab content""" + main_vbox = Box( + orientation="v", + spacing=0, + style="padding: 0; margin: 15px;" + ) + + # Set fixed size for the main container to match GUI window + main_vbox.set_size_request(620, 580) + + # Create widgets first + self.bluetooth_subtitle = Label( + markup="Find and connect to Bluetooth devices", + h_align="start" + ) + self.bluetooth_switch = Gtk.Switch() + self.scan_icon = Label(name="bluetooth-tab-icon", markup=icons.radar, style="font-size: 18px;") + self.scan_button = Button( + name="bluetooth-scan", + child=self.scan_icon, + tooltip_text="Scan for devices", + on_clicked=lambda *_: self.bluetooth_client.toggle_scan(), + style="margin:10px;" + ) + + # Header section with title and controls + header_box = CenterBox( + name="bluetooth-header", + style="margin-bottom: 16px;", + start_children=[ + Box( + orientation="v", + spacing=4, + children=[ + Label( + markup="Bluetooth", + h_align="start" + ), + self.bluetooth_subtitle, + ] + ) + ], + end_children=[ + Box( + orientation="h", + spacing=8, + children=[ + self.bluetooth_switch, + self.scan_button, + ] + ) + ] + ) + + # Set up switch + self.bluetooth_switch.set_valign(Gtk.Align.CENTER) + self.bluetooth_switch.connect("notify::active", self._on_bluetooth_switch_toggled) + + main_vbox.add(header_box) + + # Device containers + self.paired_box = Box(spacing=20, orientation="v") + self.available_box = Box(spacing=20, orientation="v", ) + + content_box = Box( + spacing=8, + orientation="v", + children=[ + Label(name="bluetooth-section", markup="Paired Devices", h_align="start"), + self.paired_box, + Label(name="bluetooth-section", markup="Available Devices", h_align="start"), + self.available_box, + ] + ) + + # Devices list in scrolled window + devices_scrolled = ScrolledWindow( + name="bluetooth-devices", + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False, + child=content_box, + ) + + devices_scrolled.set_size_request(-1, 500) + main_vbox.add(devices_scrolled) + + # Initialize bluetooth status and populate existing devices + self._update_bluetooth_status() + self._populate_existing_devices() + self.bluetooth_client.notify("enabled") + self.bluetooth_client.notify("scanning") + + return main_vbox + + def _update_bluetooth_status(self): + """Update Bluetooth status display""" + if self.bluetooth_client.enabled: + self.bluetooth_switch.set_active(True) + self.bluetooth_subtitle.set_markup( + "Find and connect to Bluetooth devices" + ) + self.scan_button.set_sensitive(True) + else: + self.bluetooth_switch.set_active(False) + self.bluetooth_subtitle.set_markup( + "Bluetooth is turned off" + ) + self.scan_button.set_sensitive(False) + + def _on_bluetooth_changed(self, *args): + """Handle bluetooth state changes""" + GLib.idle_add(self._update_bluetooth_status) + GLib.idle_add(self._refresh_all_devices) + + def _update_scan_button(self, *args): + """Update scan button appearance""" + if self.bluetooth_client.scanning: + self.scan_icon.set_markup(icons.loader) + self.scan_icon.add_style_class("scanning") + self.scan_button.add_style_class("scanning") + self.scan_button.set_tooltip_text("Stop scanning for Bluetooth devices") + else: + self.scan_icon.set_markup(icons.radar) + self.scan_icon.remove_style_class("scanning") + self.scan_button.remove_style_class("scanning") + self.scan_button.set_tooltip_text("Scan for Bluetooth devices") + + def _populate_existing_devices(self): + """Populate existing devices when tab is created""" + if not self.bluetooth_client or not self.bluetooth_client.enabled: + return + + devices = self.bluetooth_client.devices + + + for device in devices: + self.on_device_added(self.bluetooth_client, device.address) + + def on_device_added(self, client: BluetoothClient, address: str): + """Handle device added""" + # Check if device already exists to prevent duplicates + if address in self.device_widgets: + return + + if not (device := client.get_device(address)): + return + + slot = BluetoothDeviceSlot(device) + + # Store the slot in device_widgets to track it + self.device_widgets[address] = slot + + if device.paired or device.trusted: + return self.paired_box.add(slot) + else: + return self.available_box.add(slot) + + def on_device_removed(self, client: BluetoothClient, address: str): + """Handle device removed""" + if address in self.device_widgets: + slot = self.device_widgets[address] + # Remove from UI + parent = slot.get_parent() + if parent: + parent.remove(slot) + # Remove from tracking + del self.device_widgets[address] + + def _on_bluetooth_switch_toggled(self, switch, gparam): + """Handle Bluetooth switch toggle""" + is_active = switch.get_active() + self.bluetooth_client.enabled = is_active + + # Refresh devices when bluetooth is toggled + if is_active: + GLib.timeout_add(1000, self._populate_existing_devices) + else: + self._clear_all_devices() + + def _clear_all_devices(self): + """Clear all device widgets""" + for child in self.paired_box.get_children(): + self.paired_box.remove(child) + for child in self.available_box.get_children(): + self.available_box.remove(child) + self.device_widgets.clear() + + def _refresh_all_devices(self): + """Clear and repopulate all devices""" + self._clear_all_devices() + self._populate_existing_devices() + + + + diff --git a/config/settings/gui.py b/config/settings/gui.py index 1a2f9f4..db31ccd 100644 --- a/config/settings/gui.py +++ b/config/settings/gui.py @@ -18,6 +18,8 @@ from PIL import Image from config.data import APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY from config.settings.utils import backup_and_replace, bind_vars, start_config +from config.settings.wifi_tab import WiFiTab +from config.settings.bluetooth_tab import BluetoothTab gi.require_version("Gtk", "3.0") @@ -37,6 +39,10 @@ class HyprConfGUI(Window): self.show_lock_checkbox = show_lock_checkbox self.show_idle_checkbox = show_idle_checkbox + # Initialize WiFi and Bluetooth tabs + self.wifi_tab = WiFiTab() + self.bluetooth_tab = BluetoothTab() + root_box = Box(orientation="v", spacing=10, style="margin: 10px;") self.add(root_box) @@ -49,12 +55,19 @@ class HyprConfGUI(Window): v_expand=False, h_expand=False, ) - + self.wifi_tab_content = self.wifi_tab.create_wifi_tab() + self.bluetooth_tab_content = self.bluetooth_tab.create_bluetooth_tab() self.key_bindings_tab_content = self.create_key_bindings_tab() self.appearance_tab_content = self.create_appearance_tab() self.system_tab_content = self.create_system_tab() self.about_tab_content = self.create_about_tab() + self.tab_stack.add_titled( + self.wifi_tab_content, "Wifi", "Wifi" + ) + self.tab_stack.add_titled( + self.bluetooth_tab_content, "Bluetooth", "Bluetooth" + ) self.tab_stack.add_titled( self.key_bindings_tab_content, "key_bindings", "Key Bindings" ) diff --git a/config/settings/wifi_tab.py b/config/settings/wifi_tab.py new file mode 100644 index 0000000..3e7361d --- /dev/null +++ b/config/settings/wifi_tab.py @@ -0,0 +1,525 @@ +import gi +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.entry import Entry +from fabric.widgets.label import Label +from fabric.widgets.scrolledwindow import ScrolledWindow +from gi.repository import GLib, Gtk + +from services.network import NetworkClient +from utils import icons +import subprocess + +gi.require_version("Gtk", "3.0") + + +class WiFiTab: + """WiFi network management tab for settings""" + + def __init__(self): + self.network_client = NetworkClient() + self.access_point_widgets = {} + self.password_entries = {} + self.connecting_to = None + + # 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() + + def _on_network_ready(self, *args): + """Called when network client is ready""" + self._setup_network_signals() + self._refresh_networks() + + def _setup_network_signals(self): + """Setup signals for WiFi device""" + if 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_ap_added) + self.network_client.wifi_device.connect("ap-removed", self._on_ap_removed) + + def _on_network_changed(self, *args): + """Called when network state changes""" + self._refresh_networks() + + def _on_wifi_changed(self, *args): + """Called when WiFi state changes""" + self._refresh_networks() + + def _on_ap_added(self, device, ap): + """Called when access point is added""" + self._refresh_networks() + + def _on_ap_removed(self, device, ap): + """Called when access point is removed""" + self._refresh_networks() + + def create_wifi_tab(self): + """Create the WiFi tab content""" + main_vbox = Box( + orientation="v", + spacing=0, + style="padding: 0; margin: 15px;" + ) + + # Set fixed size for the main container to match GUI window + main_vbox.set_size_request(620, 580) # Match GUI window content area + + # Header section with title and controls + header_box = Box( + orientation="h", + spacing=12, + ) + + # WiFi section + wifi_section = Box(orientation="v", spacing=4) + + # WiFi title + wifi_title = Label( + markup="Wi-Fi", + h_align="start" + ) + wifi_section.add(wifi_title) + + # WiFi subtitle + self.wifi_subtitle = Label( + markup="Find and connect to Wi-Fi networks", + h_align="start" + ) + wifi_section.add(self.wifi_subtitle) + + header_box.add(wifi_section) + + # Spacer to push controls to the right + spacer = Box() + spacer.set_hexpand(True) + header_box.add(spacer) + + # Controls section + controls_box = Box(orientation="h", spacing=12) + + # WiFi toggle switch + self.wifi_switch = Gtk.Switch() + self.wifi_switch.set_valign(Gtk.Align.CENTER) + self.wifi_switch.connect("notify::active", self._on_wifi_switch_toggled) + controls_box.add(self.wifi_switch) + + # Refresh button + self.refresh_button = Button() + self.refresh_icon = Label(name="wifi-tab-icon", markup=icons.reload, style="font-size: 18px;") + self.refresh_button.add(self.refresh_icon) + self.refresh_button.set_size_request(40, 40) + self.refresh_button.set_tooltip_text("Refresh networks") + self.refresh_button.set_style("margin:10px;") + self.refresh_button.connect("clicked", self._on_refresh_clicked) + controls_box.add(self.refresh_button) + + header_box.add(controls_box) + main_vbox.add(header_box) + + # Networks list in scrolled window + self.networks_scrolled = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False, + ) + + # Set size to match available space (580 - header space ≈ 500) + self.networks_scrolled.set_size_request(-1, 500) + + self.networks_container = Box( + orientation="v", + spacing=0, + ) + self.networks_scrolled.add(self.networks_container) + main_vbox.add(self.networks_scrolled) + + # Initialize the display + GLib.timeout_add(100, self._refresh_networks) + + return main_vbox + + def _refresh_networks(self): + """Refresh the networks display""" + # Clear existing widgets + for child in self.networks_container.get_children(): + self.networks_container.remove(child) + self.access_point_widgets.clear() + self.password_entries.clear() + + # Update WiFi status + self._update_wifi_status() + + # Show available networks if WiFi is enabled + if (self.network_client.wifi_device and + self.network_client.wifi_device.wireless_enabled): + self._populate_networks() + + self.networks_container.show_all() + return False + + def _update_wifi_status(self): + """Update WiFi status display""" + if not self.network_client.wifi_device: + self.wifi_subtitle.set_markup( + "No WiFi device available" + ) + self.wifi_switch.set_sensitive(False) + return + + if self.network_client.wifi_device.wireless_enabled: + self.wifi_switch.set_active(True) + self.wifi_subtitle.set_markup( + "Find and connect to Wi-Fi networks" + ) + self.refresh_button.set_sensitive(True) + else: + self.wifi_switch.set_active(False) + self.wifi_subtitle.set_markup( + "Wi-Fi is turned off" + ) + self.refresh_button.set_sensitive(False) + + def _populate_networks(self): + """Populate the networks list""" + if not self.network_client.wifi_device: + self._show_status_message("No WiFi device available", "error") + return + + access_points = self.network_client.wifi_device.access_points + if not access_points: + self._show_status_message( + "No networks found\nClick 'Scan for Networks' to refresh", + "info" + ) + return + + # Sort access points by signal strength (connected first, then by strength) + sorted_aps = sorted( + access_points, + key=lambda ap: (not ap.is_active, -ap.strength) + ) + + for ap in sorted_aps: + self._create_network_widget(ap) + + def _show_status_message(self, message, message_type="info"): + """Show a status message in the networks container""" + status_box = Box( + orientation="v", + spacing=16, + style="padding: 60px;" + ) + status_box.set_halign(Gtk.Align.CENTER) + status_box.set_valign(Gtk.Align.CENTER) + + # Icon based on message type + if message_type == "error": + icon_markup = icons.cloud_off + message_label = Label( + markup=f"{message}", + h_align="center", + style="color: var(--error);" + ) + elif message_type == "scanning": + icon_markup = icons.loader + message_label = Label( + markup=f"{message}", + h_align="center", + style="color: var(--blue);" + ) + else: # info + icon_markup = icons.wifi_off + message_label = Label( + markup=f"{message}", + h_align="center", + style="color: var(--outline);" + ) + + icon_label = Label(name="wifi-tab-icon", markup=icon_markup, style="font-size: 18px;") + icon_label.set_halign(Gtk.Align.CENTER) + status_box.add(icon_label) + message_label.set_line_wrap(True) + message_label.set_justify(Gtk.Justification.CENTER) + message_label.set_halign(Gtk.Align.CENTER) + status_box.add(message_label) + + self.networks_container.add(status_box) + + def _create_network_widget(self, ap): + """Create widget for a single network""" + # Main container for this network (Box to allow adding password entry) + network_container = Box(orientation="v", spacing=0) + + # Clickable network button + network_button = Button( + style="background-color: transparent; border: none; padding: 0; margin: 0;" + ) + network_button.connect("clicked", lambda btn, access_point=ap: self._on_network_clicked(access_point)) + + # Network info row + info_box = Box(orientation="h", spacing=12) + info_box.set_margin_left(16) + info_box.set_margin_right(16) + info_box.set_margin_top(12) + info_box.set_margin_bottom(12) + + # WiFi signal icon + strength = ap.strength + if strength >= 75: + icon_markup = icons.wifi_3 + elif strength >= 50: + icon_markup = icons.wifi_2 + elif strength >= 25: + icon_markup = icons.wifi_1 + else: + icon_markup = icons.wifi_0 + + signal_label = Label(name="wifi-tab-icon", markup=icon_markup, style="font-size: 24px;") + signal_label.set_size_request(30, -1) + info_box.add(signal_label) + + # Network name + network_name = ap.ssid + if ap.is_active: + network_markup = f"{network_name}\nConnected" + network_label = Label(markup=network_markup, h_align="start", style="color: var(--foreground);") + else: + network_markup = f"{network_name}" + network_label = Label(markup=network_markup, h_align="start", style="color: var(--foreground);") + network_label.set_line_wrap(True) + info_box.add(network_label) + + # Spacer to push lock icon to the right + spacer = Box() + spacer.set_hexpand(True) + info_box.add(spacer) + + # Lock icon for secured networks + if ap.requires_password: + lock_label = Label(name="wifi-tab-icon", markup=icons.lock, style="font-size: 22px;") + info_box.add(lock_label) + + network_button.add(info_box) + network_container.add(network_button) + + # Store reference + self.access_point_widgets[ap.ssid] = { + 'container': network_container, + 'button': network_button, + 'info_box': info_box, + 'network_label': network_label, + 'ap': ap + } + + self.networks_container.add(network_container) + + def _on_network_clicked(self, ap): + """Handle network item click""" + if ap.is_active: + # If connected, disconnect from this network + self._disconnect_network(ap) + else: + # If not connected, check if password is needed + if ap.requires_password and ap.ssid not in self.password_entries: + # Show password entry interface + self._show_password_entry(ap) + else: + # Connect directly (either no password needed or password already entered) + self._connect_network(ap) + + def _on_wifi_switch_toggled(self, switch, gparam): + """Handle WiFi switch toggle""" + if self.network_client.wifi_device: + is_active = switch.get_active() + self.network_client.wifi_device.wireless_enabled = is_active + GLib.timeout_add(500, self._refresh_networks) + + def _on_refresh_clicked(self, button): + """Handle refresh button click""" + if self.network_client.wifi_device and self.network_client.wifi_device.wireless_enabled: + self.refresh_icon.set_markup(icons.loader) + button.set_sensitive(False) + + # Clear current networks and show scanning message + for child in self.networks_container.get_children(): + self.networks_container.remove(child) + self._show_status_message("Scanning for networks...", "scanning") + self.networks_container.show_all() + + self.network_client.wifi_device.scan() + + # Re-enable button after scan + GLib.timeout_add(3000, lambda: [ + self.refresh_icon.set_markup(icons.reload), + button.set_sensitive(True), + self._refresh_networks() + ]) + + def _connect_network(self, ap): + """Connect to a network""" + # Get password if available (either from entry or stored) + password = "" + if ap.requires_password: + if ap.ssid in self.password_entries: + # Get password from stored entry widget + password_widget = self.password_entries[ap.ssid] + if hasattr(password_widget, 'get_text'): + password = password_widget.get_text() + else: + password = str(password_widget) # If it's stored as string + else: + # No password available, this shouldn't happen if called correctly + print(f"Warning: No password available for {ap.ssid}") + return + + self.connecting_to = ap.ssid + + # Hide password entry if shown + if ap.ssid in self.password_entries: + self._hide_password_entry(ap) + + # Update network widget to show connecting state + if ap.ssid in self.access_point_widgets: + network_label = self.access_point_widgets[ap.ssid]['network_label'] + network_label.set_markup(f"{ap.ssid}\nConnecting...") + network_label.set_style("color: var(--blue);") + + try: + # Connect to network + result = self.network_client.wifi_device.connect_to_wifi(ap, password) + if result is False: + # Connection failed immediately + self._show_connection_error(ap.ssid, "Failed to connect") + self._reset_connection_state(ap.ssid) + else: + # Reset button after timeout if still connecting + GLib.timeout_add(15000, lambda: self._reset_connection_state(ap.ssid)) + except Exception as e: + self._show_connection_error(ap.ssid, f"Connection error: {str(e)}") + self._reset_connection_state(ap.ssid) + + def _show_connection_error(self, ssid, error_message): + + try: + + subprocess.run([ + "notify-send", + "WiFi Connection Failed", + f"Failed to connect to {ssid}\n{error_message}", + "--icon=network-wireless-offline", + "--urgency=normal", + "--app-name=WiFi" + ], check=False) + except Exception as e: + print(f"Failed to send connection error notification: {e}") + + def _show_password_entry(self, ap): + """Show password entry for a network""" + if ap.ssid in self.access_point_widgets: + container = self.access_point_widgets[ap.ssid]['container'] + + # Check if password entry is already shown + if len(container.get_children()) > 1: + return # Already showing password entry + + # Password entry row with theme styling + password_box = Box( + orientation="h", + spacing=8, + style="padding: 8px; margin: 4px 16px 8px 16px; border-radius: 8px;" + ) + + password_entry = Entry( + placeholder_text="Enter network password", + style="background-color: var(--surface-bright); border: 1px solid var(--outline); border-radius: 8px;" + ) + password_entry.set_visibility(False) # Hide password characters + password_entry.set_hexpand(True) + password_entry.connect("activate", lambda entry, access_point=ap: self._on_password_entered(access_point, entry)) + password_box.add(password_entry) + + # Show/Hide password toggle + show_password_btn = Button( + style="border-radius: 8px;" + ) + self.show_password_icon = Label(name="wifi-tab-icon", markup=icons.spy,style="font-size: 18px;") + show_password_btn.add(self.show_password_icon) + show_password_btn.set_tooltip_text("Show/Hide password") + show_password_btn.connect("clicked", lambda btn, entry=password_entry: self._toggle_password_visibility(entry, btn)) + password_box.add(show_password_btn) + + connect_btn = Button( + style="border-radius: 8px;" + ) + connect_icon = Label(name="wifi-tab-icon", markup=icons.connect,style="font-size: 18px;") + connect_btn.add(connect_icon) + connect_btn.connect("clicked", lambda btn, access_point=ap, entry=password_entry: self._on_password_entered(access_point, entry)) + password_box.add(connect_btn) + + cancel_btn = Button( + style="border-radius: 8px;" + ) + cancel_icon = Label(name="wifi-tab-icon", markup=icons.cancel,style="font-size: 18px;") + cancel_btn.add(cancel_icon) + cancel_btn.connect("clicked", lambda btn, access_point=ap: self._hide_password_entry(access_point)) + password_box.add(cancel_btn) + + container.add(password_box) + container.show_all() + + # Store reference and focus + self.password_entries[ap.ssid] = password_entry + GLib.timeout_add(100, lambda: password_entry.grab_focus()) + + def _toggle_password_visibility(self, entry, button): + """Toggle password visibility""" + current_visibility = entry.get_visibility() + entry.set_visibility(not current_visibility) + # Use spy icon for hidden, key icon for visible + self.show_password_icon.set_markup(icons.key if not current_visibility else icons.spy) + + def _on_password_entered(self, ap, entry): + """Handle password entry""" + password = entry.get_text() + self.password_entries[ap.ssid] = password + self._hide_password_entry(ap) + self._connect_network(ap) + + def _hide_password_entry(self, ap): + """Hide password entry for a network""" + if ap.ssid in self.access_point_widgets: + container = self.access_point_widgets[ap.ssid]['container'] + children = container.get_children() + if len(children) > 1: # Remove password entry box + container.remove(children[-1]) + + def _disconnect_network(self, ap): + """Disconnect from a network""" + if self.network_client.wifi_device: + self.network_client.wifi_device.disconnect_wifi() + GLib.timeout_add(1000, self._refresh_networks) + + def _reset_connection_state(self, ssid): + """Reset connection state for a network""" + if ssid in self.access_point_widgets: + # Reset the network label to normal state + ap = self.access_point_widgets[ssid]['ap'] + network_label = self.access_point_widgets[ssid]['network_label'] + if ap.is_active: + network_markup = f"{ap.ssid}\nConnected" + network_label.set_style("color: var(--foreground);") + else: + network_markup = f"{ap.ssid}" + network_label.set_style("color: var(--foreground);") + network_label.set_markup(network_markup) + + self.connecting_to = None + self._refresh_networks() + return False diff --git a/main.css b/main.css index 4d53f78..70ab211 100644 --- a/main.css +++ b/main.css @@ -52,6 +52,9 @@ tooltip > * { border-radius: 10px; } + + + #workspaces, #workspaces-container, #launcher, @@ -68,3 +71,4 @@ tooltip > * { #dock { box-shadow: 0 0 3px alpha(black, 0.7); } +