Skip to content

Commit 4664e8a

Browse files
committed
Initial commit
0 parents  commit 4664e8a

File tree

16 files changed

+1634
-0
lines changed

16 files changed

+1634
-0
lines changed

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Virtual environment files
2+
bin/
3+
lib/
4+
lib64/
5+
lib64
6+
include/
7+
pyvenv.cfg
8+
9+
# Python cache and build files
10+
__pycache__/
11+
*.py[cod]
12+
*.egg-info/
13+
dist/
14+
15+
# Other IDE/Editor files
16+
.idea/
17+
.vscode/
18+
*.swp

LICENSE

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
GNU GENERAL PUBLIC LICENSE
2+
Version 3, 29 June 2007
3+
4+
Copyright (c) 2025 Aditya Pratap Singh
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see <https://www.gnu.org/licenses/>.

app/main.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
import gi
3+
import argparse
4+
import sys
5+
from network_service import NetworkService
6+
from ui.main_window import NetworkManagerWindow
7+
gi.require_version("Gtk", "4.0")
8+
from gi.repository import Gtk
9+
from ui.styles import StyleManager
10+
11+
# Application version
12+
__version__ = "1.0.0"
13+
14+
class NetworkManagerApp(Gtk.Application):
15+
"""Main application class"""
16+
17+
def __init__(self):
18+
super().__init__(application_id="com.network.manager")
19+
20+
def do_activate(self):
21+
"""Application activation"""
22+
StyleManager.apply_styles()
23+
win = NetworkManagerWindow(self)
24+
win.present()
25+
26+
def parse_arguments():
27+
"""Parse command line arguments"""
28+
parser = argparse.ArgumentParser(
29+
description="Network Manager - A GTK4 network management application",
30+
prog="network-manager"
31+
)
32+
33+
parser.add_argument(
34+
"-v", "--version",
35+
action="version",
36+
version=f"%(prog)s {__version__}"
37+
)
38+
39+
return parser.parse_args()
40+
41+
if __name__ == "__main__":
42+
try:
43+
# Parse command line arguments first
44+
args = parse_arguments()
45+
46+
# Check if NetworkManager is available before starting the app
47+
if not NetworkService.check_networkmanager():
48+
sys.exit(1)
49+
50+
# If we get here, NetworkManager is available, so start the app
51+
app = NetworkManagerApp()
52+
app.run()
53+
except KeyboardInterrupt:
54+
print("Application stopped manually.")
55+
except SystemExit:
56+
# argparse calls sys.exit() for version/help, let it pass through
57+
pass

app/models.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Data models for the network manager application"""
2+
3+
from dataclasses import dataclass
4+
from enum import Enum
5+
from typing import Optional
6+
7+
class WiFiState(Enum):
8+
"""Enum for WiFi states"""
9+
OFF = "off"
10+
ON = "on"
11+
SCANNING = "scanning"
12+
CONNECTING = "connecting"
13+
14+
@dataclass
15+
class NetworkInfo:
16+
"""Data class for network information"""
17+
ssid: str
18+
signal: int
19+
requires_password: bool
20+
is_connected: bool = False
21+
bssid: Optional[str] = None
22+
frequency: Optional[int] = None
23+
channel: Optional[int] = None
24+
rate: Optional[int] = None
25+
mode: Optional[str] = None
26+
security: Optional[str] = None
27+
28+
@classmethod
29+
def from_wifi_device(cls, wifi_device):
30+
"""Create NetworkInfo from nmcli wifi device"""
31+
return cls(
32+
ssid=wifi_device.ssid,
33+
signal=wifi_device.signal,
34+
requires_password=bool(wifi_device.security),
35+
is_connected=wifi_device.in_use,
36+
bssid=wifi_device.bssid,
37+
frequency=wifi_device.freq,
38+
channel=wifi_device.chan,
39+
rate=wifi_device.rate,
40+
mode=wifi_device.mode,
41+
security=wifi_device.security
42+
)

app/network_service.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import re
2+
import time
3+
import nmcli
4+
import subprocess
5+
import shutil
6+
from typing import List, Tuple, Optional
7+
8+
# Import the new NetworkInfo model from models
9+
from models import NetworkInfo
10+
11+
nmcli.disable_use_sudo()
12+
13+
class NmcliExtensions:
14+
"""Extended functionalities for nmcli package"""
15+
16+
@staticmethod
17+
def connect_to_open_or_saved_wifi(device_control_instance, ssid: str) -> None:
18+
"""Connect to a known WiFi network without requiring password"""
19+
cmd = ['device', 'wifi', 'connect', ssid]
20+
21+
try:
22+
result = device_control_instance._syscmd.nmcli(cmd)
23+
24+
failure_patterns = [
25+
r'Connection activation failed:',
26+
r'Error: Connection activation failed',
27+
r'Error: No network with SSID',
28+
r'Error: Failed to add/activate new connection'
29+
]
30+
31+
for pattern in failure_patterns:
32+
if re.search(pattern, result):
33+
raise nmcli._exception.ConnectionActivateFailedException(
34+
f'Connection activation failed for {ssid}'
35+
)
36+
37+
print(f"Successfully connected to {ssid}")
38+
39+
except nmcli._exception.ConnectionActivateFailedException:
40+
raise
41+
except Exception as e:
42+
print(f"Failed to connect to {ssid}: {e}")
43+
raise
44+
45+
@staticmethod
46+
def wifi_force_rescan(device_control_instance) -> bool:
47+
"""Force a WiFi rescan to refresh available networks"""
48+
cmd = ['device', 'wifi', 'list', '--rescan', 'yes']
49+
50+
51+
try:
52+
device_control_instance._syscmd.nmcli(cmd)
53+
print("WiFi rescan completed successfully")
54+
return True
55+
except Exception as e:
56+
print(f"WiFi rescan failed: {e}")
57+
raise
58+
59+
class NetworkService:
60+
"""Service class to handle network operations"""
61+
62+
@staticmethod
63+
def get_wifi_status() -> bool:
64+
"""Check Wi-Fi status"""
65+
try:
66+
return nmcli.radio.wifi()
67+
except Exception as e:
68+
print(f"Error getting Wi-Fi status: {e}")
69+
return False
70+
71+
@staticmethod
72+
def toggle_wifi(state: bool) -> bool:
73+
"""Enable or disable Wi-Fi"""
74+
try:
75+
current_state = nmcli.radio.wifi()
76+
if current_state == state:
77+
return True
78+
79+
if state:
80+
nmcli.radio.wifi_on()
81+
else:
82+
nmcli.radio.wifi_off()
83+
return True
84+
except Exception as e:
85+
print(f"Error toggling Wi-Fi: {e}")
86+
return False
87+
88+
@staticmethod
89+
def scan_networks(force_rescan: bool = True) -> List[NetworkInfo]:
90+
"""Scan for available networks with optional force rescan"""
91+
networks = []
92+
try:
93+
if force_rescan:
94+
NmcliExtensions.wifi_force_rescan(nmcli.device)
95+
time.sleep(0.5) # Allow rescan to complete
96+
97+
for wifi in nmcli.device.wifi():
98+
if not wifi.ssid:
99+
continue
100+
101+
# Use the new NetworkInfo.from_wifi_device method
102+
network = NetworkInfo.from_wifi_device(wifi)
103+
networks.append(network)
104+
105+
print(f"Found {len(networks)} networks")
106+
107+
except Exception as e:
108+
print(f"Error scanning networks: {e}")
109+
110+
return networks
111+
112+
@staticmethod
113+
def is_wifi_known(ssid: str) -> bool:
114+
"""Check if WiFi network is already known/saved"""
115+
try:
116+
connections = nmcli.connection()
117+
return any(conn.name == ssid for conn in connections)
118+
except Exception as e:
119+
print(f"Error checking known networks: {e}")
120+
return False
121+
122+
@staticmethod
123+
def forget_wifi(ssid: str) -> Tuple[bool, str]:
124+
"""
125+
Forget a saved WiFi network by SSID.
126+
127+
Args:
128+
ssid: The SSID of the network to forget
129+
130+
Returns:
131+
Tuple of (success: bool, message: str)
132+
Where success indicates if the operation was successful,
133+
and message provides details about the result or error
134+
"""
135+
try:
136+
# First check if the network exists in known connections
137+
if not NetworkService.is_wifi_known(ssid):
138+
return False, f"No saved network found with SSID: '{ssid}'"
139+
140+
# Try to delete the connection
141+
try:
142+
nmcli.connection.delete(ssid)
143+
144+
# Verify deletion was successful
145+
if NetworkService.is_wifi_known(ssid):
146+
return False, f"Failed to forget network '{ssid}' - still exists after deletion"
147+
148+
return True, f"Successfully forgot network: '{ssid}'"
149+
150+
except nmcli._exception.ConnectionDeleteException as e:
151+
# Handle specific nmcli deletion errors
152+
error_msg = str(e).strip()
153+
if "no such connection" in error_msg.lower():
154+
return False, f"No saved network found with SSID: '{ssid}'"
155+
return False, f"Failed to forget network '{ssid}': {error_msg}"
156+
157+
except Exception as e:
158+
error_msg = f"Unexpected error forgetting network '{ssid}': {str(e)}"
159+
print(error_msg)
160+
return False, error_msg
161+
162+
@staticmethod
163+
def connect_to_network(ssid: str, password: Optional[str] = None) -> Tuple[bool, str]:
164+
"""Connect to a network using improved connection method"""
165+
try:
166+
if password:
167+
nmcli.device.wifi_connect(ssid, password)
168+
else:
169+
NmcliExtensions.connect_to_open_or_saved_wifi(nmcli.device, ssid)
170+
171+
return True, "Connected successfully"
172+
173+
except nmcli._exception.ConnectionActivateFailedException as e:
174+
return False, f"Connection activation failed: {str(e)}"
175+
except Exception as e:
176+
return False, f"Connection error: {str(e)}"
177+
178+
@staticmethod
179+
def disconnect_network(ssid:str) -> Tuple[bool, str]:
180+
"""Disconnect to a network using improved disconnection method"""
181+
try:
182+
nmcli.connection.down(ssid)
183+
return True, "Disconnected Successfully"
184+
except Exception as e:
185+
return False, f"Connection error: {str(e)}"
186+
187+
188+
@staticmethod
189+
def get_wifi_details(ssid: str):
190+
"""Get detailed information about a specific wifi network"""
191+
try:
192+
for wifi in nmcli.device.wifi():
193+
if wifi.ssid == ssid:
194+
return NetworkInfo.from_wifi_device(wifi)
195+
return None
196+
except Exception as e:
197+
print(f"Error getting wifi details: {e}")
198+
return None
199+
200+
@staticmethod
201+
def check_networkmanager():
202+
"""Check if NetworkManager is available on the system"""
203+
# Check if nmcli command is available
204+
if not shutil.which("nmcli"):
205+
print("Error: NetworkManager is not installed or not available in PATH.")
206+
print("Please install NetworkManager to use this application.")
207+
return False
208+
209+
# Check if NetworkManager service is running
210+
try:
211+
result = subprocess.run(
212+
["nmcli", "general", "status"],
213+
capture_output=True,
214+
text=True,
215+
timeout=5
216+
)
217+
if result.returncode != 0:
218+
print("Error: NetworkManager is not running.")
219+
print("Please start the NetworkManager service.")
220+
return False
221+
except subprocess.TimeoutExpired:
222+
print("Error: NetworkManager is not responding.")
223+
return False
224+
except Exception as e:
225+
print(f"Error: Could not check NetworkManager status: {e}")
226+
return False
227+
228+
return True
229+

app/ui/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)