Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,53 @@
#!/usr/bin/env python3
import gi
import argparse
import sys

import gi
from network_service import NetworkService
from ui.main_window import NetworkManagerWindow

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
from ui.styles import StyleManager

# Application version
__version__ = "1.0.0"


class NetworkManagerApp(Gtk.Application):
"""Main application class"""

def __init__(self):
super().__init__(application_id="com.network.manager")

def do_activate(self):
"""Application activation"""
StyleManager.apply_styles()
win = NetworkManagerWindow(self)
win.present()


def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="Network Manager - A GTK4 network management application",
prog="network-manager"
)

parser.add_argument(
"-v", "--version",
action="version",
version=f"%(prog)s {__version__}"
prog="network-manager",
)


parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")

return parser.parse_args()


if __name__ == "__main__":
try:
# Parse command line arguments first
args = parse_arguments()

# Check if NetworkManager is available before starting the app
if not NetworkService.check_networkmanager():
sys.exit(1)

# If we get here, NetworkManager is available, so start the app
app = NetworkManagerApp()
app.run()
Expand Down
87 changes: 47 additions & 40 deletions app/ui/main_window.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import gi

from models import NetworkInfo, WiFiState

from ui.network_list import NetworkListWidget

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gdk, GLib
from typing import Optional

from network_service import NetworkService
from gi.repository import Gdk, GLib, Gtk
from network_service import NetworkService

from ui.dialogs import PasswordDialog
from ui.network_details import NetworkDetailsWidget
from ui.wifi_off import WiFiOffWidget
from ui.dialogs import PasswordDialog


class NetworkManagerWindow(Gtk.ApplicationWindow):
"""Main application window"""

def __init__(self, app):
super().__init__(application=app)
self.set_title("Network Manager")
self.set_default_size(450, 350)

self.current_state = WiFiState.OFF
self.current_view = "list" # "list" or "details"
self._setup_ui()
Expand All @@ -28,54 +31,54 @@ def __init__(self, app):
key_controller = Gtk.EventControllerKey.new()
key_controller.connect("key-pressed", self._on_esc_pressed)
self.add_controller(key_controller)

def _setup_ui(self):
"""Setup the main UI"""
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
main_box.set_margin_top(15)
main_box.set_margin_bottom(20)
main_box.set_margin_start(20)
main_box.set_margin_end(20)

main_box.append(self._create_wifi_toggle())
main_box.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))

self.content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, vexpand=True)
main_box.append(self.content_box)

self.set_child(main_box)

def _create_wifi_toggle(self) -> Gtk.Box:
"""Create the WiFi toggle section"""
self.wifi_switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self.wifi_switch.connect("state-set", self._on_wifi_toggled)

wifi_label = Gtk.Label(label="Wi-Fi", xalign=0, name="wifi-label")

toggle_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
toggle_box.append(wifi_label)
toggle_box.append(Gtk.Box(hexpand=True)) # Spacer
toggle_box.append(self.wifi_switch)

return toggle_box

def _on_wifi_toggled(self, switch, state):
"""Handle WiFi toggle"""
current_state = NetworkService.get_wifi_status()

if current_state == state:
return

if NetworkService.toggle_wifi(state):
self._update_wifi_state(scan_immediately=state)
else:
switch.set_active(current_state)

def _update_wifi_state(self, scan_immediately=False, initial_load=False):
"""Update the UI based on WiFi state"""
wifi_status = NetworkService.get_wifi_status()
self.wifi_switch.set_active(wifi_status)

if wifi_status:
self.current_state = WiFiState.ON
if self.current_view == "list":
Expand All @@ -84,90 +87,94 @@ def _update_wifi_state(self, scan_immediately=False, initial_load=False):
self.current_state = WiFiState.OFF
self.current_view = "list"
self._show_wifi_off()

def _show_network_list(self, scan_immediately=False):
"""Show the network list widget"""
self.current_view = "list"
self._clear_content()

self.network_list = NetworkListWidget(self._on_network_selected, self._on_network_details)
self.content_box.append(self.network_list)

if scan_immediately:
GLib.timeout_add(500, lambda: self.network_list.start_scan())

def _show_network_details(self, network: NetworkInfo):
"""Show the network details widget"""
self.current_view = "details"
self._clear_content()

self.network_details = NetworkDetailsWidget(network, self._on_back_to_list)
self.content_box.append(self.network_details)

def _on_back_to_list(self):
"""Handle back button click from details view"""
self._show_network_list(scan_immediately=True)

def _show_wifi_off(self):
"""Show the WiFi off widget"""
self._clear_content()
wifi_off_widget = WiFiOffWidget()
self.content_box.append(wifi_off_widget)

def _clear_content(self):
"""Clear the content area"""
child = self.content_box.get_first_child()
while child:
next_child = child.get_next_sibling()
self.content_box.remove(child)
child = next_child

def _on_network_selected(self, network: NetworkInfo):
"""Handle network selection for connection"""
if NetworkService.is_wifi_known(network.ssid):
self._connect_to_network(network.ssid)
elif network.requires_password:
dialog = PasswordDialog(self, network.ssid,
lambda password: self._connect_to_network(network.ssid, password))
dialog = PasswordDialog(
self,
network.ssid,
lambda password: self._connect_to_network(network.ssid, password),
)
dialog.show()
else:
self._connect_to_network(network.ssid)

def _on_network_details(self, network: NetworkInfo):
"""Handle network details button click"""
self._show_network_details(network)

def _connect_to_network(self, ssid: str, password: Optional[str] = None):
"""Connect to a network"""
self.current_state = WiFiState.CONNECTING

def connect_thread():
success, message = NetworkService.connect_to_network(ssid, password)
GLib.idle_add(lambda: self._connection_complete(ssid, success, message))

import threading

thread = threading.Thread(target=connect_thread)
thread.daemon = True
thread.start()

def _connection_complete(self, ssid: str, success: bool, message: str):
"""Handle connection completion"""
self.current_state = WiFiState.ON

dialog = Gtk.AlertDialog()
if success:
dialog.set_message(f"Connected to {ssid}")
dialog.set_detail("Connection established successfully")
else:
dialog.set_message(f"Failed to connect to {ssid}")
dialog.set_detail(f"Error: {message}")

dialog.set_modal(True)
dialog.show(self)
if self.current_view == "list" and hasattr(self, 'network_list'):

if self.current_view == "list" and hasattr(self, "network_list"):
self.network_list.start_scan()

return False

def _on_esc_pressed(self, controller, keyval, keycode, state):
Expand Down
41 changes: 25 additions & 16 deletions app/ui/styles.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""CSS styling for the application"""

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gdk
import os

from gi.repository import Gdk, Gtk


class StyleManager:
"""Handles CSS styling for the application"""

CSS_STYLES = b"""
#wifi-label {
font-weight: bold;
font-size: 16px;
}
}

#wifi-networks-label {
font-weight: bold;
Expand All @@ -25,18 +29,18 @@ class StyleManager:
#wifi-scan-label:hover {
color: #4a90d9;
}

#scanning-label {
font-style: italic;
color: #888888;
margin: 10px;
}

#error-label {
color: #cc0000;
margin: 10px;
}

#no-networks-label {
font-style: italic;
color: #888888;
Expand All @@ -50,21 +54,21 @@ class StyleManager:
.connected-network {
background-color: rgba(74, 144, 217, 0.1);
}

#wifi-off-box {
margin: 50px;
}

#wifi-off-label {
color: #666666;
font-size: 14px;
}

.rescan-in-progress {
color: #4a90d9;
animation: pulse 1.5s infinite;
}

@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
Expand All @@ -75,30 +79,35 @@ class StyleManager:
font-size: 1.5rem;
font-weight: 500;
}

.title-4 {
font-size: 1.1rem;
font-weight: 500;
margin-bottom: 8px;
}

.dim-label {
opacity: 0.7;
font-size: 0.9rem;
}

scrollbar slider {
min-width: 6px;
min-height: 6px;
}
"""

@classmethod
def apply_styles(cls):
"""Apply CSS styles to the application"""
css_provider = Gtk.CssProvider()
css_provider.load_from_data(cls.CSS_STYLES)

# Test if the ~/.config/nmgui/style.css exists
home_path = os.path.expanduser("~")
if os.path.exists(f"{home_path}/.config/nmgui/style.css"):
css_provider.load_from_path(f"{home_path}/.config/nmgui/style.css")
else:
css_provider.load_from_data(cls.CSS_STYLES)

display = Gdk.Display.get_default()
Gtk.StyleContext.add_provider_for_display(
display, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
Expand Down