diff --git a/config/settings/bluetooth_tab.py b/config/settings/bluetooth_tab.py
deleted file mode 100644
index b93b98f..0000000
--- a/config/settings/bluetooth_tab.py
+++ /dev/null
@@ -1,450 +0,0 @@
-import subprocess
-
-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 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 = {}
- self.window_size_enforcer = None
-
- # 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 set_window_size_enforcer(self, enforcer_func):
- """Set the window size enforcer function from the main GUI"""
- self.window_size_enforcer = enforcer_func
-
- 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 tab stack dimensions
- main_vbox.set_size_request(580, 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 with size constraints
- self.paired_box = Box(spacing=20, orientation="v")
- self.paired_box.set_size_request(560, -1)
- self.available_box = Box(spacing=20, orientation="v")
- self.available_box.set_size_request(560, -1)
-
- 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,
- ],
- )
- # Set size constraints for content box
- content_box.set_size_request(560, -1)
-
- # Devices list in scrolled window
- devices_scrolled = ScrolledWindow(
- name="bluetooth-devices",
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- child=content_box,
- )
-
- # Set fixed size to prevent dynamic resizing
- devices_scrolled.set_size_request(580, 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:
- self.paired_box.add(slot)
- else:
- self.available_box.add(slot)
-
- # Enforce window size after adding device
- if self.window_size_enforcer:
- GLib.idle_add(self.window_size_enforcer)
-
- 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]
-
- # Enforce window size after removing device
- if self.window_size_enforcer:
- GLib.idle_add(self.window_size_enforcer)
-
- 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 dbf3a56..900eda7 100644
--- a/config/settings/gui.py
+++ b/config/settings/gui.py
@@ -15,11 +15,9 @@ from PIL import Image
from config.data import APP_NAME, APP_NAME_CAP
from config.settings.about_tab import AboutTab
from config.settings.appearance_tab import AppearanceTab
-from config.settings.bluetooth_tab import BluetoothTab
from config.settings.key_bindings_tab import KeyBindingsTab
from config.settings.system_tab import SystemTab
from config.settings.utils import backup_and_replace, start_config
-from config.settings.wifi_tab import WiFiTab
gi.require_version("Gtk", "3.0")
@@ -53,8 +51,6 @@ class HyprConfGUI(Window):
self.show_idle_checkbox = show_idle_checkbox
# Initialize all tabs
- self.wifi_tab = WiFiTab()
- self.bluetooth_tab = BluetoothTab()
self.key_bindings_tab = KeyBindingsTab()
self.appearance_tab = AppearanceTab(self.themes, self)
self.system_tab = SystemTab(
@@ -62,10 +58,6 @@ class HyprConfGUI(Window):
)
self.about_tab = AboutTab()
- # Pass window size enforcement method to tabs
- self.wifi_tab.set_window_size_enforcer(self.enforce_window_size)
- self.bluetooth_tab.set_window_size_enforcer(self.enforce_window_size)
-
root_box = Box(orientation="v", spacing=10, style="margin: 10px;")
# Set fixed size for root container
root_box.set_size_request(620, 620)
@@ -87,15 +79,11 @@ class HyprConfGUI(Window):
# Set fixed size for tab stack
self.tab_stack.set_size_request(580, 580)
- 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.key_bindings_tab.create_key_bindings_tab()
self.appearance_tab_content = self.appearance_tab.create_appearance_tab()
self.system_tab_content = self.system_tab.create_system_tab()
self.about_tab_content = self.about_tab.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
deleted file mode 100644
index 5fd4f2f..0000000
--- a/config/settings/wifi_tab.py
+++ /dev/null
@@ -1,568 +0,0 @@
-import subprocess
-
-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
-
-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
- self.window_size_enforcer = 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 set_window_size_enforcer(self, enforcer_func):
- """Set the window size enforcer function from the main GUI"""
- self.window_size_enforcer = enforcer_func
-
- 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 tab stack dimensions
- main_vbox.set_size_request(580, 580)
-
- # 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=False,
- v_expand=False,
- propagate_width=False,
- propagate_height=False,
- )
-
- # Set fixed size to prevent dynamic resizing (580 - header space ≈ 500)
- self.networks_scrolled.set_size_request(580, 500)
-
- self.networks_container = Box(
- orientation="v",
- spacing=0,
- )
- # Set size constraints for networks container
- self.networks_container.set_size_request(560, -1)
- 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()
- # Enforce window size after content changes
- if self.window_size_enforcer:
- GLib.idle_add(self.window_size_enforcer)
- 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 45ab6a5..31a01a9 100644
--- a/main.css
+++ b/main.css
@@ -143,60 +143,6 @@ tooltip > * {
transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
-.wavy-circle {
- color: var(--primary);
- background-color: var(--surface);
- border-color: var(--primary);
-}
-
-#matugen-switcher,
-#bluetooth-switch,
-#wifi-switch,
-#dnd-switch {
- min-width: 40px;
- min-height: 20px;
- background-color: var(--surface);
- border-radius: 15px;
- padding: 2px;
- transition: background-color 0.3s ease;
-}
-
-/* Style the switch's slider */
-#matugen-switcher slider,
-#bluetooth-switch slider,
-#wifi-switch slider,
-#dnd-switch slider {
- background-color: var(--primary);
- border-radius: 16px;
- min-width: 16px;
- min-height: 8px;
- transition:
- background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25),
- transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25);
-}
-
-/* When the switch is active (checked) */
-#matugen-switcher:checked,
-#bluetooth-switch:checked,
-#wifi-switch:checked,
-#dnd-switch:checked {
- background-color: var(--primary);
-}
-
-/* Optional: additional styling for the slider when active */
-#matugen-switcher:checked slider,
-#bluetooth-switch:checked slider,
-#wifi-switch:checked slider,
-#dnd-switch:checked slider {
- background-color: var(--shadow);
-}
-
-#matugen-switcher:checked image,
-#bluetooth-switch:checked image,
-#wifi-switch:checked image,
-#dnd-switch:checked image {
- color: var(--shadow);
-}
#bluetooth-header,
diff --git a/modules/dashboard/network.py b/modules/dashboard/network.py
index a54ad2d..508e25c 100644
--- a/modules/dashboard/network.py
+++ b/modules/dashboard/network.py
@@ -1,17 +1,68 @@
-from fabric.widgets.label import Label
+from fabric.utils import bulk_connect
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 Gtk
from modules.dashboard.tile import Tile
from services.network import NetworkClient
-
import utils.icons as icons
+class WifiAccessPointSlot(CenterBox):
+ """A widget representing a single WiFi access point in the dashboard."""
+
+ def __init__(self, access_point, network_service: NetworkClient, wifi_service, **kwargs):
+ super().__init__(name="wifi-ap-slot", **kwargs)
+ self.access_point = access_point
+ self.network_service = network_service
+ self.wifi_service = wifi_service
+
+ ssid = access_point.ssid
+ icon_name = access_point.icon
+
+ self.is_active = access_point.is_active
+
+ self.ap_icon = Image(icon_name=icon_name, size=24)
+ self.ap_label = Label(
+ label=ssid,
+ h_expand=True,
+ h_align="start",
+ ellipsization="end"
+ )
+
+ self.connect_button = Button(
+ name="wifi-connect-button",
+ label="Connected" if self.is_active else "Connect",
+ sensitive=not self.is_active,
+ on_clicked=self._on_connect_clicked,
+ style_classes=["connected"] if self.is_active else None,
+ )
+
+ self.set_start_children([
+ Box(spacing=8, h_expand=True, h_align="fill", children=[
+ self.ap_icon,
+ self.ap_label,
+ ])
+ ])
+ self.set_end_children([self.connect_button])
+
+ def _on_connect_clicked(self, *_):
+ """Handle connect button click."""
+ if not self.is_active and self.access_point.bssid:
+ self.connect_button.set_label("Connecting...")
+ self.connect_button.set_sensitive(False)
+ self.network_service.connect_wifi_bssid(self.access_point.bssid)
+
+
class Network(Tile):
+ """Network tile for the dashboard that shows WiFi status and networks."""
+
def __init__(self, **kwargs):
+ # Create status label
self.label = Label(
style_classes=["desc-label", "off"],
label="Disconnected",
@@ -26,19 +77,36 @@ class Network(Tile):
menu=True,
**kwargs,
)
- self.nm = NetworkClient()
- self.nm.connect("changed", self.handle_connection_change)
- self.nm.connect("ready", self.handle_connection_change)
+
+ # Initialize network client
+ self.network_client = NetworkClient()
+ self.network_client.connect("changed", self.handle_connection_change)
+ self.network_client.connect("ready", self.handle_connection_change)
+ self.network_client.connect("wifi_device_added", self._on_wifi_device_added)
+
+ # Initialize state
+ self._update_tile_state()
+
+ # Track if content has been created to avoid showing status during init
+ self._content_created = False
+
+ # Check if WiFi device is already available
+ if self.network_client.wifi_device:
+ self._setup_wifi_device()
def handle_connection_change(self, *_):
- # Get current network state from NetworkClient properties
+ """Handle network connection state changes."""
+ self._update_tile_state()
+
+ def _update_tile_state(self):
+ """Update tile visual state based on WiFi status."""
connected = False
con_label = "Disconnected"
- if self.nm.wifi_device and self.nm.wifi_device.active_access_point:
+ if self.network_client.wifi_device and self.network_client.wifi_device.active_access_point:
connected = True
- con_label = self.nm.wifi_device.active_access_point.ssid
- elif not self.nm.wireless_enabled:
+ con_label = self.network_client.wifi_device.active_access_point.ssid
+ elif self.network_client.wifi_device and not self.network_client.wifi_device.wireless_enabled:
con_label = "Off"
if self.state != connected:
@@ -49,14 +117,66 @@ class Network(Tile):
else:
self.remove_style_class("on")
self.add_style_class("off")
- print("State change:", connected)
+ print("WiFi state change:", connected)
self.label.set_label(con_label)
def create_content(self):
- """Create the detailed network content for the dashboard"""
- content_box = Box(
+ """Create the detailed network content for the dashboard."""
+ # Create status label
+ self.status_label = Label(
+ label="Initializing ",
+ h_expand=True,
+ visible=False,
+ h_align="center"
+ )
+
+ # Create refresh button with icon
+ self.refresh_button_icon = Label(
+ name="network-refresh-label",
+ markup=icons.reload
+ )
+ self.refresh_button = Button(
+ name="network-refresh",
+ child=self.refresh_button_icon,
+ tooltip_text="Scan for Wi-Fi networks",
+ on_clicked=self._refresh_access_points
+ )
+
+ # Create back button with chevron left icon
+ self.back_button_icon = Label(
+ name="network-back-label",
+ markup=icons.chevron_left
+ )
+ self.back_button = Button(
+ name="network-back",
+ child=self.back_button_icon,
+ tooltip_text="Back to notifications",
+ on_clicked=self._on_back_clicked
+ )
+
+ # Create header
+ header_box = CenterBox(
+ name="network-header",
+ start_children=[self.back_button],
+ center_children=[Label(name="network-title", label="Wi-Fi Networks")],
+ end_children=[self.refresh_button]
+ )
+
+ # Create access points list
+ self.ap_list_box = Box(orientation="v", spacing=4)
+ scrolled_window = ScrolledWindow(
+ name="network-ap-scrolled-window",
+ child=self.ap_list_box,
+ h_expand=True,
+ v_expand=True,
+ propagate_width=False,
+ propagate_height=False,
+ )
+
+ # Main container
+ main_container = Box(
name="network-content",
orientation="v",
spacing=8,
@@ -64,192 +184,108 @@ class Network(Tile):
v_expand=True,
h_align="fill",
style_classes=["tile-content"],
+ children=[
+ header_box,
+ self.status_label,
+ scrolled_window,
+ ]
)
- # Header with WiFi toggle
- header_box = Box(
- orientation="h",
- spacing=12,
- h_expand=True,
- h_align="fill",
- )
+ # Initialize button states
+ self.refresh_button.set_sensitive(False)
- wifi_label = Label(
- markup="Wi-Fi",
- h_align="start",
- h_expand=True,
- )
+ # Mark content as created
+ self._content_created = True
- self.wifi_switch = Gtk.Switch(
- name="wifi-switch",
- active=self.nm.wireless_enabled if self.nm.wifi_device else False,
- )
- self.wifi_switch.connect("notify::active", self._on_wifi_toggle)
+ # Now update UI state since content is ready
+ if self.network_client.wifi_device:
+ self._update_wifi_status_ui()
- header_box.children = [wifi_label, self.wifi_switch]
- content_box.add(header_box)
+ return main_container
- # Networks list
- self.networks_container = Box(
- orientation="v",
- spacing=4,
- h_expand=True,
- )
+ def _on_wifi_device_added(self, *_):
+ """Handle when WiFi device is added."""
+ self._setup_wifi_device()
- # Scrolled window for networks
- networks_scroll = ScrolledWindow(
- h_scrollbar_policy="never",
- v_scrollbar_policy="automatic",
- h_expand=True,
- v_expand=True,
- child=self.networks_container,
- )
- networks_scroll.set_size_request(-1, 200)
+ def _setup_wifi_device(self):
+ """Setup WiFi device connections and initial state."""
+ if self.network_client.wifi_device:
+ self.network_client.wifi_device.connect("changed", self._load_access_points)
+ self.network_client.wifi_device.connect("notify::wireless-enabled", self._update_wifi_status_ui)
- content_box.add(networks_scroll)
+ # Only update UI if content has been created
+ if self._content_created and hasattr(self, 'refresh_button'):
+ self._update_wifi_status_ui()
+ else:
+ # Only show error if content has been created
+ if self._content_created and hasattr(self, 'status_label'):
+ self.status_label.set_label("Wi-Fi device not available.")
+ self.status_label.set_visible(True)
+ if hasattr(self, 'refresh_button'):
+ self.refresh_button.set_sensitive(False)
- # Scan button
- scan_button = Button(
- label="Scan for Networks",
- h_align="center",
- on_clicked=self._on_scan_clicked,
- )
- content_box.add(scan_button)
+ def _update_wifi_status_ui(self, *_):
+ """Update WiFi status UI elements."""
+ if not hasattr(self, 'refresh_button') or not self._content_created:
+ return # Content not created yet
- # Populate networks initially
- self._refresh_networks()
+ if self.network_client.wifi_device:
+ enabled = self.network_client.wifi_device.wireless_enabled
+ self.refresh_button.set_sensitive(enabled)
- return content_box
+ if not enabled:
+ self.status_label.set_label("Wi-Fi disabled.")
+ self.status_label.set_visible(True)
+ self._clear_ap_list()
+ else:
+ self.refresh_button.set_sensitive(False)
- def _on_wifi_toggle(self, *_):
- """Handle WiFi toggle"""
- if self.nm.wifi_device:
- self.nm.wifi_device.toggle_wifi()
- self._refresh_networks()
+ def _on_back_clicked(self, *_):
+ """Handle back button click - return to default (notifications) view."""
+ if self.dashboard_instance:
+ self.dashboard_instance.reset_to_default()
- def _on_scan_clicked(self, *_):
- """Handle scan button click"""
- if self.nm.wifi_device:
- self.nm.wifi_device.scan()
- self._refresh_networks()
+ def _refresh_access_points(self, *_):
+ """Refresh access points list."""
+ if self.network_client.wifi_device and self.network_client.wifi_device.wireless_enabled:
+ self.status_label.set_label("Scanning for Wi-Fi networks...")
+ self.status_label.set_visible(True)
+ self._clear_ap_list()
+ self.network_client.wifi_device.scan()
+ return False
- def _refresh_networks(self):
- """Refresh the networks list"""
- # Clear existing networks
- for child in self.networks_container.get_children():
- self.networks_container.remove(child)
+ def _clear_ap_list(self):
+ """Clear the access points list."""
+ for child in self.ap_list_box.get_children():
+ child.destroy()
- if not self.nm.wifi_device or not self.nm.wifi_device.wireless_enabled:
- no_wifi_label = Label(
- label="WiFi is disabled",
- h_align="center",
- style_classes=["dim-label"],
- )
- self.networks_container.add(no_wifi_label)
+ def _load_access_points(self, *_):
+ """Load and display access points."""
+ if not hasattr(self, 'ap_list_box'):
+ return # Content not created yet
+
+ if not self.network_client.wifi_device or not self.network_client.wifi_device.wireless_enabled:
+ self._clear_ap_list()
+ self.status_label.set_label("Wi-Fi disabled.")
+ self.status_label.set_visible(True)
return
- access_points = self.nm.wifi_device.access_points
+ self._clear_ap_list()
+
+ access_points = self.network_client.wifi_device.access_points
+
if not access_points:
- no_networks_label = Label(
- label="No networks found\nClick 'Scan for Networks' to refresh",
- h_align="center",
- style_classes=["dim-label"],
- )
- self.networks_container.add(no_networks_label)
- 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_item(ap)
-
- self.networks_container.show_all()
-
- def _create_network_item(self, ap):
- """Create a network item widget"""
- network_box = Box(
- orientation="h",
- spacing=8,
- h_expand=True,
- style_classes=["network-item"],
- )
-
- # Signal strength icon
- signal_strength = min(100, max(0, ap.strength))
- if signal_strength >= 75:
- signal_icon = icons.wifi_3
- elif signal_strength >= 50:
- signal_icon = icons.wifi_2
- elif signal_strength >= 25:
- signal_icon = icons.wifi_1
+ self.status_label.set_label("No Wi-Fi networks found.")
+ self.status_label.set_visible(True)
else:
- signal_icon = icons.wifi_0
+ self.status_label.set_visible(False)
+ # Sort access points by connection status (connected first) then by signal strength
+ sorted_aps = sorted(access_points, key=lambda x: (not x.is_active, -x.strength))
+ for access_point in sorted_aps:
+ slot = WifiAccessPointSlot(
+ access_point, self.network_client, self.network_client.wifi_device
+ )
+ self.ap_list_box.add(slot)
+ self.ap_list_box.show_all()
- signal_label = Label(
- markup=signal_icon,
- style_classes=["network-signal"],
- )
- network_box.add(signal_label)
- # Network name and status
- info_box = Box(
- orientation="v",
- h_expand=True,
- h_align="start",
- )
-
- if ap.is_active:
- name_markup = f"{ap.ssid}"
- status_label = Label(
- label="Connected",
- h_align="start",
- style_classes=["network-status", "connected"],
- )
- info_box.add(Label(markup=name_markup, h_align="start"))
- info_box.add(status_label)
- else:
- name_label = Label(
- label=ap.ssid,
- h_align="start",
- style_classes=["network-name"],
- )
- info_box.add(name_label)
-
- network_box.add(info_box)
-
- # Lock icon for secured networks
- if ap.requires_password:
- lock_label = Label(
- markup=icons.lock,
- style_classes=["network-lock"],
- )
- network_box.add(lock_label)
-
- # Make the whole item clickable
- network_button = Button(
- child=network_box,
- style_classes=["network-button"],
- on_clicked=lambda *_: self._on_network_clicked(ap),
- )
-
- self.networks_container.add(network_button)
-
- def _on_network_clicked(self, ap):
- """Handle network item click"""
- if ap.is_active:
- # Disconnect from current network
- if self.nm.wifi_device:
- self.nm.wifi_device.disconnect_wifi()
- else:
- # Connect to network (simplified - would need password handling for secured networks)
- if self.nm.wifi_device:
- try:
- self.nm.wifi_device.connect_to_access_point(ap)
- except Exception as e:
- print(f"Failed to connect to {ap.ssid}: {e}")
-
- # Refresh networks after action
- self._refresh_networks()
diff --git a/utils/icons.py b/utils/icons.py
index ed187ea..074570d 100644
--- a/utils/icons.py
+++ b/utils/icons.py
@@ -89,7 +89,6 @@ coffee: str = ""
notifications: str = ""
notifications_dots: str = ""
-blur: str = chr(0xF00B5)
arrow_head: str = ""
wifi_off: str = ""
bluetooth_off: str = ""