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