Skip to content
Open
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
153 changes: 153 additions & 0 deletions app/ethernet_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import nmcli
from typing import Literal, overload
from nmcli.data.device import DeviceDetails


class EthernetDetails:
device: str
hwaddr: str
name: str
ipaddr: str
gateway_addr: str
state: Literal["connected", "disconnected", "unavailable"]

def __init__(self):
# pass device object to parser
details = EthernetService.get_device_details()
if details != None:
self._parse_data(details)

def _parse_data(self, eth_dev: DeviceDetails) -> None:
"""parses the information from eth_dev and add to properties"""
self.device = eth_dev.get("GENERAL.DEVICE")
self.hwaddr = eth_dev.get("GENERAL.HWADDR")
self.name = eth_dev.get("GENERAL.CONNECTION")
self.ipaddr = (
eth_dev.get("IP4.ADDRESS[1]")
or eth_dev.get("IP6.ADDRESS[1]")
or "unavailable"
)
self.gateway_addr = (
eth_dev.get("IP4.GATEWAY") or eth_dev.get("IP6.GATEWAY") or "unavailable"
)

# the format of GENERAL.STATE is "int (str)", e.g. "100 (connected)"
state: int = int(eth_dev.get("GENERAL.STATE").split(" ")[0])

# check for connected and disconnected, the rest is unavailable
if state == 30:
self.state = "disconnected"
elif state == 100:
self.state = "connected"
else:
self.state = "unavailable"


class EthernetService:
@staticmethod
def get_device_details() -> DeviceDetails | None:
"""Get the DeviceDetails object of the ethernet device"""
# find device with ethernet type
for dev in nmcli.device.show_all():
if dev.get("GENERAL.TYPE") == "ethernet":
return dev

return None

@staticmethod
def get_device() -> str | None:
"""Get the ethernet device's identifier"""
details = EthernetService.get_device_details()

if details == None:
return None

return details.get("GENERAL.DEVICE")

@overload
@staticmethod
def get_ethernet_status() -> bool:
"""Get the status of the ethernet, True for connected, if not, False. Will always return False if ethernet is not available."""
pass

@overload
@staticmethod
def get_ethernet_status(details: EthernetDetails) -> bool:
"""Get the status of the ethernet from the details, True for connected, if not, False. Will always return False if ethernet is not available."""
pass

def get_ethernet_status(details: EthernetDetails | None = None) -> bool:
if details == None:
details = EthernetService.get_device_details()

# no ethernet device available
if details == None:
return False

state = int(details.get("GENERAL.STATE").split(" ")[0])

if state == 100:
return True

return False

# if details is an EthernetDetails object
else:
if details.state == "connected":
return True

return False

@overload
@staticmethod
def is_ethernet_available() -> bool:
"""Check if ethernet is available and able to be connected. This is different from status."""
pass

@overload
@staticmethod
def is_ethernet_available(details: EthernetDetails) -> bool:
"""Check if ethernet is available and able to be connected from the details. This is different from status."""
pass

def is_ethernet_available(details: EthernetDetails | None = None) -> bool:
if details == None:
details = EthernetService.get_device_details()

# no ethernet device available
if details == None:
return False

state = int(details.get("GENERAL.STATE").split(" ")[0])

# if state is disconnected or connected, then its available
if state == 30 or state == 100:
return True

return False

# if details is an EthernetDetails object
else:
if details.state != "unavailable":
return True

return False

@staticmethod
def toggle_ethernet(state: bool) -> bool:
"""Enable or disable Ethernet"""
try:
current_state = EthernetService.get_ethernet_status()
if current_state == state:
return True

if state:
nmcli._syscmd.nmcli(["device", "connect", EthernetService.get_device()])
else:
nmcli._syscmd.nmcli(
["device", "disconnect", EthernetService.get_device()]
)
return True
except Exception as e:
print(f"Error toggling Ethernet: {e}")
return False
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
108 changes: 108 additions & 0 deletions app/ui/ethernet_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Callable
import gi

from ui.utils import UIUtils
from ethernet_service import EthernetService, EthernetDetails

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


class EthernetDetailsWidget(Gtk.Box):
content: Gtk.Box
refresh_label: Gtk.Label
name_label: Gtk.Label
spinner: Gtk.Spinner
switch: Gtk.Switch

def __init__(self, ethernet_switch: Gtk.Switch):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=10)

self.content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)

# an ethernet_switch parameter is passed to the widget so that we can set the switch's state on update
self.switch = ethernet_switch

self._create_header()
self.append(self.content)
self._show_content()

def _create_header(self):
self.refresh_label = Gtk.Label(label="Refresh", name="ethernet-scan-label")
self.refresh_label.set_cursor_from_name("pointer")
self.refresh_label.add_controller(self._create_click_controller(self.update))

self.spinner = Gtk.Spinner()
self.spinner.set_size_request(20, 20)

self.name_label = Gtk.Label(
label=EthernetDetails().name or "Not Connected",
xalign=0,
name="ethernet-name-label",
)

header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
header_box.append(self.name_label)
header_box.append(Gtk.Box(hexpand=True)) # Spacer
header_box.append(self.spinner)
header_box.append(self.refresh_label)

self.append(header_box)

def _create_click_controller(self, callback: Callable):
"""Create a click controller for a label"""
controller = Gtk.GestureClick()
controller.set_button(0)
controller.connect("pressed", lambda *args: callback())
return controller

def _show_content(self):
if not EthernetService.is_ethernet_available():
return

details = EthernetDetails()

self.content.append(
UIUtils.create_detail_row(
"Network Status",
details.state.title(),
"network-transmit-receive-symbolic",
)
)
self.content.append(
UIUtils.create_detail_row(
"IP Address", details.ipaddr, "network-workgroup-symbolic"
)
)
self.content.append(
UIUtils.create_detail_row(
"MAC Address", details.hwaddr, "network-wired-symbolic"
)
)

def _clear_content(self):
child = self.content.get_first_child()
while child:
next_child = child.get_next_sibling()
self.content.remove(child)
child = next_child

def update(self):
self.spinner.start()
self.refresh_label.set_sensitive(False)
self.refresh_label.add_css_class("rescan-in-progress")

self.name_label.set_label(EthernetDetails().name or "Not Connected")

if EthernetService.is_ethernet_available():
self._clear_content()
self._show_content()
self.switch.set_sensitive(True)
else:
self._clear_content()
self.switch.set_active(False)
self.switch.set_sensitive(False)

self.spinner.stop()
self.refresh_label.set_sensitive(True)
self.refresh_label.remove_css_class("rescan-in-progress")
Loading