7 Commits
main ... V1

Author SHA1 Message Date
meshya
8ba9bc8b17 update readme for V1 2025-10-11 20:14:18 +03:30
meshya
e3442d7aa4 feat: Implement main window and profile management for Namban DNS Manager
- Added `MainWindow` class to manage the main application interface.
- Introduced `ProfileManager` for handling DNS profiles with JSON storage.
- Created `DNSProfile` and `DNSServer` models to represent DNS configurations.
- Developed UI components including `ProfileRow`, `ProfileEditSheet`, and `SettingsPage`.
- Integrated proxy settings retrieval using `ProxyManager`.
- Removed legacy files and refactored code for improved structure and readability.
- Updated dependencies in `pyproject.toml` for enhanced functionality and compatibility.
2025-10-11 01:36:30 +03:30
meshya
32095de02f update pkgfile 2024-11-16 22:39:38 +03:30
meshya
cb306b7f2d update readme 2024-11-16 22:38:27 +03:30
meshya
ca902d39b3 resolve package problem 2024-11-16 22:37:10 +03:30
meshya
b187f6c842 solve package problem 2024-11-16 22:25:28 +03:30
meshya
41ba74101a add contributors file 2024-11-02 01:25:05 +03:30
32 changed files with 717 additions and 1051 deletions

5
CONTRIBUTORS.txt Normal file
View File

@@ -0,0 +1,5 @@
ali gholipour
ars101
hussein eslami
meshya
sohrab behdani

View File

@@ -6,26 +6,19 @@ A simple gui tool for set dns settings based on GTK4, related to [parchlinux pro
![Screenshot from 2024-06-08 14-46-56](https://github.com/parchlinuxB/namban/assets/59795638/22e368c0-6277-4689-b39b-8dd4c961441e)
## run
## Branch V1
It not released in a poppular repository yet, to use it you may use parchlinux repositories `pacman -S namban` or download package files from release page, or use a develpment environment.
This branch is about changing app ui, refactor, add DoH and DoT support. contributors are welcome.
## develop environment
1. python > 3.10
2. Gtk > 4.10
3. PyGObject
4. A glass of milk for mental peace
3. Adw
4. PyGObject
5. A lot of milk for mental peace
## To Do
- [x] Make a Icon! ( Done by HUSS )
- [x] Fix text color in darkmode
- [x] Send maintainer to therapy ( Done by sohrab and HUSS )
- [x] publish in parch repos ( Done by sohrab )
- [ ] publish in AUR
- [X] Add build file for debian ( Done by ARS101 )
- [X] Create a CI/CD 4 debian ( Done by ARS101 )
- [X] Add build file for redhat ( Done by sohrab )
- [ ] Error Message for wrong input [#5](https://github.com/parchlinuxB/namban/issues/5)
## Far To Do
- [ ] DoH & DoT support
- [ ] DoH & DoT support.
- [ ] Rewrite with new design in js.
- [ ] Make sure it's compatible with previous version.

View File

@@ -1,6 +1,6 @@
pkgname=namban
pkgver=0.3
pkgrel=5
pkgrel=7
pkgdesc="use custom dns has never been hard"
arch=('any')
url="https://github.com/parchlinuxb/namban"

View File

@@ -1,35 +0,0 @@
import gi
gi.require_version("Gtk","4.0")
gi.require_version("Gdk","4.0")
from gi.repository import Gtk
from view import window
import settings
import storage
import sys
import control
import core
import checks
class application(Gtk.Application, control.control, storage.storage, core.core):
def __init__(self):
Gtk.Application.__init__(self,
application_id=settings.PACKAGE_NAME
)
self.connect('activate', self.activate)
self.connectedProfile = None
@property
def data(self):
return self.read()
def activate(self,*args):
win = window(app=self)
win.present()
def run(self,*d,**dd):
super().run(*d,**dd)
self.disconnectProfiles()
def main() :
return application().run([])

View File

View File

@@ -1,41 +0,0 @@
class LowFields(Exception):
def __init__(self, *fields):
super().__init__(
f"This fields is esential: {', '.join(fields)}"
)
self.fields = fields
class Field:
def __init__(self, optional=False):
self.optional = optional
self.name = None
def set_name(self,_name):
self.name = _name
class Model:
def __init_subclass__(cls) -> None:
cls._fields:list[Field] = []
for name, obj in cls.__dict__.items():
if isinstance(obj, Field):
obj.set_name(name)
cls._fields.append(obj)
def __init__(self,**wargs) -> None:
errorFields = []
for f in self._fields:
if f.name not in wargs:
if f.optional:
wargs[f.name] = None
else :
errorFields.append(f)
if errorFields :
raise LowFields(*errorFields)
for name, val in wargs.items():
if name in list(map(lambda x:x.name, self._fields)):
setattr(
self, name, val
)
def __eq__(self,another):
class oooooo1:...
class oooooo2:...
for f in self._fields:
if getattr(self,f.name,oooooo1) != getattr(another,f.name,oooooo2):
return False
return True

View File

@@ -1,35 +0,0 @@
import os
import settings
import re
import json
from core.basePromise import base_promise
def checkPromises():
promiseFiles = os.listdir(settings.PROMISES_PATH)
for pf in filter(lambda n: re.match(r".+\.promise",os.path.basename(n)) ,promiseFiles):
try:
file = open(pf,"r")
row = file.read()
file.close()
js = json.loads(row)
name = js['name']
PromiseClass = base_promise.FindPromiseClass(name)
promise = PromiseClass.loadfromfile(js['dist'])
promise.handle()
except:
os.remove(pf)
def checkPaths():
def makSure(f):
if not os.path.exists(f):
os.mkdir(f)
for p in settings._imps:
makSure(p)
def checkStartupService():
os.system(
'systemctl enable namban-startup-check'
)

View File

@@ -1,31 +0,0 @@
import domain as domain
import storage
class control:
def addProfile(self, p:domain.profile):
data = self.read()
data.profiles.append(p)
self.write(data)
self.window.update()
def deleteProfile(self, p:domain.profile):
data = self.read()
data.profiles.remove(p)
self.write(data)
self.window.update()
def editProfile(self, p:domain.profile, newp:domain.profile):
def indexOfProf(all,p):
i = 0
for c in all:
if c==p:
return i
i += 1
data = self.read()
data.profiles[indexOfProf(data.profiles,p)] = newp
self.write(data)
def connectProfile(self, profile:domain.profile):
self.connectedProfile = profile
self.systemDnsSet(profile)
self.window.update()
def disconnectProfiles(self):
self.connectedProfile = None
self.systemDnsSet(None)

View File

@@ -1,15 +0,0 @@
import domain
from .strategy import resolvd
class core:
def systemDnsSet(self,profile:domain.profile):
if profile == None:
self.turnOffDns()
return
self.currentStrategy = resolvd(profile)
self.currentStrategy.connect()
def turnOffDns(self):
if self.currentStrategy:
self.currentStrategy.disconnect()
self.currentStrategy = None

View File

@@ -1,39 +0,0 @@
import os
import settings
import json
import hashlib
class base_promise:
def __init__(self, **d) -> None:
for name, v in d.items():
setattr(self,name,v)
dist = self.dump2file()
fp = settings.PROMISES_PATH/(hashlib.sha256(bytes(dist,"UTF-8")).hexdigest() + ".promise")
self.filePath = fp
f = open(fp, "w+")
f.write(
json.dumps( {"name":type(self).__name__ ,"dist":dist} )
)
f.close()
os.chmod(fp, 0o400)
subs = []
fields = []
def handle(self):
os.remove(self.filePath)
def dump2file(self):
d = {}
for f in self.fields:
d[f] = getattr(self,f)
return json.dumps(d)
@classmethod
def loadfromfile(cls,data):
d = json.loads(data)
obj = cls(**d)
return obj
def __init_subclass__(cls) -> None:
base_promise.subs.append(cls)
@classmethod
def FindPromiseClass(cls, name):
try:
return list(filter(lambda c:c.__name__ == name,cls.subs))[0]
except:
return

View File

@@ -1,6 +0,0 @@
class base_strategy:
def __init__(self, profile) -> None:
self.promises = []
self.profile = profile
def connect (self):...
def disconnect(self):...

View File

@@ -1,16 +0,0 @@
import os
from .basePromise import base_promise
class resolvd(base_promise):
fields = ['path']
def __init__(self, path="",**d):
super().__init__(path=path,**d)
def handle(self):
os.system(f'mv {self.path} /etc/systemd/resolved.conf')
super().handle()
class resolv_conf(base_promise):
fields = ['path']
def __init__(self, path="",**d):
super().__init__(path=path,**d)
def handle(self):
os.system(f'mv {self.path} /etc/resolv.conf')
super().handle()

View File

@@ -1,63 +0,0 @@
from .baseSterategy import base_strategy
import domain
import os
import settings
from .promises import resolvd as resolvdPromise, resolv_conf as resolvConfPromise
class resolvd(base_strategy):
def check4resovConfFile(self):
def isOk():
d = open('/etc/resolv.conf','r').read()
for line in d.splitlines():
try:
line = line.strip()
if line[:10] == 'nameserver':
return (line[10:]).strip() == '127.0.0.53'
except:...
return False
if not isOk():
resolvconffile = f'''
#Generated By NAMBAN
#Connect to systemd-resolvd
nameserver 127.0.0.53
'''.strip()
os.system(
"cp /etc/resolv.conf " + str(settings.APP_FILES_PATH/'oldconffiles/resolv.conf') + " -r"
)
with open('/etc/resolv.conf', 'w+') as f:
f.write(resolvconffile)
f.close()
self.promises.append(
resolvConfPromise((settings.APP_FILES_PATH/'oldconffiles/resolv.conf').__str__())
)
def generateResolvdConf(self):
profile = self.profile
c = f'''
# Generated By NAMBAN
[Resolve]
DNS={profile.server1.url}
'''.strip()
if profile.server2:
c += f'\nFallbackDNS={profile.server2.url}'
return c
def connect(self):
self.check4resovConfFile()
os.system(f"cp /etc/systemd/resolved.conf "+str(settings.APP_FILES_PATH/'oldconffiles/resolved.conf') + " -r")
with open("/etc/systemd/resolved.conf",'w+') as f:
f.write(
self.generateResolvdConf()
)
f.close()
os.system('systemctl daemon-reload')
os.system('systemctl restart systemd-resolved')
self.promises.append(
resolvdPromise((settings.APP_FILES_PATH/'oldconffiles/resolved.conf').__str__())
)
def disconnect(self):
while len(self.promises):
p = self.promises.pop()
p.handle()
os.system('systemctl daemon-reload')
os.system('systemctl restart systemd-resolved')

82
src/dns_manager.py Normal file
View File

@@ -0,0 +1,82 @@
import os
import subprocess
from pathlib import Path
from typing import List, Optional
from models import DNSProfile, DNSType
class DNSManager:
def __init__(self):
self.config_path = Path("/etc/systemd/resolved.conf")
self.backup_path = Path(f"/tmp/namban_resolved_backup_{os.getuid()}.conf")
self.current_profile: Optional[DNSProfile] = None
def backup_current_config(self) -> bool:
try:
if self.config_path.exists():
subprocess.run(['pkexec', 'cp', str(self.config_path), str(self.backup_path)], check=True, capture_output=True)
return True
except subprocess.CalledProcessError:
pass
return False
def restore_config(self) -> bool:
try:
if self.backup_path.exists():
subprocess.run(['pkexec', 'mv', str(self.backup_path), str(self.config_path)], check=True, capture_output=True)
self.reload_systemd_resolved()
return True
except subprocess.CalledProcessError:
pass
return False
def apply_profile(self, profile: DNSProfile) -> bool:
try:
if not self.backup_current_config():
pass
config_content = self._generate_resolved_conf(profile)
with subprocess.Popen(['pkexec', 'tee', str(self.config_path)], stdin=subprocess.PIPE, text=True,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as process:
_, stderr = process.communicate(input=config_content)
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, 'tee', stderr=stderr)
self.reload_systemd_resolved()
self.current_profile = profile
return True
except Exception:
self.restore_config()
return False
def _generate_resolved_conf(self, profile: DNSProfile) -> str:
lines = ["# Generated by Namban DNS Manager", "[Resolve]"]
dns_servers = []
is_doh = any(s.dns_type == DNSType.DOH for s in profile.servers)
is_dot = any(s.dns_type == DNSType.DOT for s in profile.servers)
for server in profile.servers:
dns_servers.append(server.primary)
if server.secondary:
dns_servers.append(server.secondary)
if dns_servers:
lines.append(f"DNS={' '.join(dns_servers)}")
if is_doh:
lines.append("DNSOverHTTPS=yes")
elif is_dot:
lines.append("DNSOverTLS=opportunistic")
lines.extend(["DNSSEC=allow-downgrade", "DNSStubListener=yes", "Cache=yes", ""])
return '\n'.join(lines)
def reload_systemd_resolved(self):
subprocess.run(['pkexec', 'systemctl', 'restart', 'systemd-resolved'], check=True, capture_output=True)
def get_current_dns(self) -> List[str]:
try:
result = subprocess.run(['resolvectl', 'status'], capture_output=True, text=True, check=True)
dns_servers = [line.split(':')[1].strip() for line in result.stdout.split('\n') if 'Current DNS Server:' in line]
if not dns_servers:
dns_servers = [s for line in result.stdout.split('\n') if 'DNS Servers:' in line for s in line.split(':')[1].strip().split()]
return dns_servers
except (subprocess.CalledProcessError, FileNotFoundError):
return []

View File

@@ -1,23 +0,0 @@
from base.domain import Model,Field
class server(Model):
type = Field()
url = Field()
def __str__(self):
return self.url
class profile(Model):
server1 = Field()
server2 = Field(optional=True)
name = Field(optional=True)
def __str__(self):
return self.name | self.server1.__str__()
class appData:
def __init__(self, profiles:list[server]) -> None:
self.profiles = profiles
class app:
def __init__(self, data:appData):
self.data = data
self.connectedProfile = None

61
src/main.py Normal file
View File

@@ -0,0 +1,61 @@
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
gi.require_version('Gio', '2.0')
import sys
import logging
from gi.repository import Gtk, Adw, Gio
from main_window import MainWindow
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(name)s:%(message)s')
class NambanApplication(Adw.Application):
def __init__(self):
super().__init__(application_id="com.parchlinux.namban", flags=Gio.ApplicationFlags.FLAGS_NONE)
def do_activate(self):
win = self.get_active_window() or MainWindow(application=self)
win.present()
def do_startup(self):
Adw.Application.do_startup(self)
Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.DEFAULT)
settings_action = Gio.SimpleAction.new("settings", None)
settings_action.connect("activate", self._on_settings)
self.add_action(settings_action)
about_action = Gio.SimpleAction.new("about", None)
about_action.connect("activate", self._on_about)
self.add_action(about_action)
quit_action = Gio.SimpleAction.new("quit", None)
quit_action.connect("activate", lambda *_: self.quit())
self.add_action(quit_action)
self.set_accels_for_action("app.quit", ["<Ctrl>q"])
def _on_settings(self, _, __):
win = self.get_active_window()
if win:
win._show_settings_sheet()
def _on_about(self, _, __):
Adw.AboutWindow(
transient_for=self.get_active_window(), application_name="Namban DNS Manager",
application_icon="com.parchlinux.namban", developer_name="Parch Linux Team & Contributors",
version="2.0.0", developers=["Meshya", "Sohrab Behdani"],
copyright="© 2024 Parch Linux Team", license_type=Gtk.License.GPL_3_0_ONLY,
website="https://github.com/parchlinuxb/namban",
issue_url="https://github.com/parchlinuxb/namban/issues"
).present()
def main():
try:
return NambanApplication().run(sys.argv)
except Exception as e:
logging.critical(f"Application failed to start: {e}", exc_info=True)
return 1
if __name__ == "__main__":
sys.exit(main())

167
src/main_window.py Normal file
View File

@@ -0,0 +1,167 @@
import gi
from typing import List, Optional
from gi.repository import Gtk, Adw, GLib, Gio
from models import DNSProfile
from dns_manager import DNSManager
from profile_manager import ProfileManager
from ui_components import ProfileRow, ProfileEditSheet, SettingsPage
class MainWindow(Adw.ApplicationWindow):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dns_manager = DNSManager()
self.profile_manager = ProfileManager()
self.profile_rows: List[ProfileRow] = []
self.active_profile: Optional[DNSProfile] = None
self.set_title("Namban DNS Manager")
self.set_default_size(450, 600)
self._build_ui()
self._load_profiles()
self._update_status()
GLib.timeout_add_seconds(10, self._update_status)
def _build_ui(self):
self.bottom_sheet = Adw.BottomSheet()
self.toast_overlay = Adw.ToastOverlay(child=self.bottom_sheet)
self.set_content(self.toast_overlay)
self.bottom_sheet.set_content(self._build_main_content())
def _build_main_content(self):
toolbar_view = Adw.ToolbarView()
header = Adw.HeaderBar(title_widget=Adw.WindowTitle(title="Namban DNS Manager"))
add_button = Gtk.Button(icon_name="list-add-symbolic", tooltip_text="Add New Profile")
add_button.connect('clicked', self._on_add_profile)
header.pack_end(add_button)
menu_button = Gtk.MenuButton(icon_name="open-menu-symbolic", menu_model=Gio.Menu.new())
menu_button.get_menu_model().append("Settings", "app.settings")
menu_button.get_menu_model().append("About", "app.about")
menu_button.get_menu_model().append("Quit", "app.quit")
header.pack_end(menu_button)
toolbar_view.add_top_bar(header)
status_group = Adw.PreferencesGroup(title="Current Status")
self.status_row = Adw.ActionRow(title="DNS Status", subtitle="Loading...")
status_group.add(self.status_row)
self.profiles_group = Adw.PreferencesGroup(title="DNS Profiles", description="Select a profile to apply")
page = Adw.PreferencesPage()
page.add(status_group)
page.add(self.profiles_group)
toolbar_view.set_content(Gtk.ScrolledWindow(child=page, hscrollbar_policy='never'))
return toolbar_view
def _load_profiles(self):
for row in self.profile_rows:
self.profiles_group.remove(row)
self.profile_rows.clear()
for profile in self.profile_manager.profiles:
row = ProfileRow(profile,
on_activate=self._on_profile_activate,
on_edit=self._on_profile_edit,
on_delete=self._on_profile_delete)
self.profiles_group.add(row)
self.profile_rows.append(row)
def _update_status(self):
current_dns = self.dns_manager.get_current_dns()
self.status_row.set_subtitle(", ".join(current_dns) or "System default")
return True
def _on_profile_activate(self, profile: DNSProfile, state: bool):
if state:
if self.dns_manager.apply_profile(profile):
self.toast_overlay.add_toast(Adw.Toast.new(f"Applied: {profile.name}"))
self.active_profile = profile
for row in self.profile_rows:
if row.profile != profile:
row.set_active(False)
else:
self.toast_overlay.add_toast(Adw.Toast.new("Failed to apply profile"))
for row in self.profile_rows:
if row.profile == profile:
row.set_active(False)
break
elif self.active_profile == profile:
if self.dns_manager.restore_config():
self.toast_overlay.add_toast(Adw.Toast.new("Restored default DNS"))
self.active_profile = None
else:
self.toast_overlay.add_toast(Adw.Toast.new("Failed to restore settings"))
for row in self.profile_rows:
if row.profile == profile:
row.set_active(True)
break
self._update_status()
def _show_profile_sheet(self, old_profile: Optional[DNSProfile] = None):
sheet_content = ProfileEditSheet(profile=old_profile)
toolbar_view = Adw.ToolbarView()
title = "Edit Profile" if old_profile else "Add New Profile"
header = Adw.HeaderBar(title_widget=Adw.WindowTitle(title=title))
toolbar_view.add_top_bar(header)
toolbar_view.set_content(sheet_content)
cancel_button = Gtk.Button(label="Cancel")
cancel_button.connect('clicked', lambda _: self.bottom_sheet.set_open(False))
header.pack_start(cancel_button)
save_button = Gtk.Button(label="Save", css_classes=["suggested-action"])
header.pack_end(save_button)
def on_save(_):
new_profile = sheet_content.get_profile()
if new_profile:
if old_profile:
self._on_profile_updated(old_profile, new_profile)
else:
self._on_profile_added(new_profile)
self.bottom_sheet.set_open(False)
else:
self.toast_overlay.add_toast(Adw.Toast.new("Profile Name and Primary DNS are required."))
save_button.connect('clicked', on_save)
self.bottom_sheet.set_sheet(toolbar_view)
self.bottom_sheet.set_open(True)
def _show_settings_sheet(self):
settings_content = SettingsPage()
toolbar_view = Adw.ToolbarView()
header = Adw.HeaderBar(title_widget=Adw.WindowTitle(title="Settings"))
toolbar_view.add_top_bar(header)
clamp = Adw.Clamp(maximum_size=400, tightening_threshold=300)
clamp.set_child(settings_content)
toolbar_view.set_content(clamp)
close_button = Gtk.Button(label="Done")
close_button.connect('clicked', lambda _: self.bottom_sheet.set_open(False))
header.pack_end(close_button)
self.bottom_sheet.set_sheet(toolbar_view)
self.bottom_sheet.set_open(True)
def _on_add_profile(self, _):
self._show_profile_sheet()
def _on_profile_edit(self, profile: DNSProfile):
self._show_profile_sheet(old_profile=profile)
def _on_profile_delete(self, profile: DNSProfile):
self.profile_manager.remove_profile(profile)
self._load_profiles()
self.toast_overlay.add_toast(Adw.Toast.new(f"Profile '{profile.name}' removed"))
if self.active_profile == profile:
self.dns_manager.restore_config()
self.active_profile = None
self._update_status()
def _on_profile_added(self, profile: DNSProfile):
self.profile_manager.add_profile(profile)
self._load_profiles()
self.toast_overlay.add_toast(Adw.Toast.new(f"Profile '{profile.name}' created"))
def _on_profile_updated(self, old_profile: DNSProfile, new_profile: DNSProfile):
self.profile_manager.update_profile(old_profile, new_profile)
self._load_profiles()
self.toast_overlay.add_toast(Adw.Toast.new(f"Profile '{new_profile.name}' updated"))

22
src/models.py Normal file
View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional, List
class DNSType(Enum):
STANDARD = "standard"
DOH = "doh"
DOT = "dot"
@dataclass
class DNSServer:
name: str
primary: str
secondary: Optional[str] = None
dns_type: DNSType = DNSType.STANDARD
doh_url: Optional[str] = None
description: Optional[str] = None
class DNSProfile:
def __init__(self, name: str, servers: List[DNSServer]):
self.name = name
self.servers = servers

View File

@@ -1,13 +0,0 @@
import application
import os
import sys
import checks
if __name__ == "__main__":
if os.getgid() != 0:
print('Run it as root!')
sys.exit(1)
checks.checkStartupService()
checks.checkPaths()
sys.exit(
application.main()
)

View File

@@ -1,61 +0,0 @@
{
"--comment": [
"This file is only for debug, effective one stored in:",
"package/files/rootfs/etc/namban/config.json",
"See also src/settings.py Line 13 dear aligholipour :)"
],
"profiles": [
{
"server1": {
"type": "dns",
"url": "1.1.1.1"
},
"name": "debug mode",
"server2": {
"type": "dns",
"url": "1.0.0.1"
}
},
{
"server1": {
"type": "dns",
"url": "8.8.8.8"
},
"name": "google",
"server2": {
"type": "dns",
"url": "8.8.4.4"
}
},{
"server1": {
"type": "dns",
"url": "10.202.10.202"
},
"name": "403 online",
"server2": {
"type": "dns",
"url": "10.202.10.102"
}
},{
"server1": {
"type": "dns",
"url": "178.22.122.100"
},
"name": "shecan",
"server2": {
"type": "dns",
"url": "185.51.200.2"
}
},{
"server1": {
"type": "dns",
"url": "78.157.42.101"
},
"name": "electro",
"server2": {
"type": "dns",
"url": "78.157.42.100"
}
}
]
}

63
src/profile_manager.py Normal file
View File

@@ -0,0 +1,63 @@
import json
import os
from pathlib import Path
from typing import List
from models import DNSProfile, DNSServer, DNSType
class ProfileManager:
def __init__(self):
self.config_dir = Path.home() / '.config' / 'namban'
self.config_file = self.config_dir / 'profiles.json'
self.ensure_config_dir()
self.profiles = self.load_profiles()
def ensure_config_dir(self):
self.config_dir.mkdir(parents=True, exist_ok=True)
def load_profiles(self) -> List[DNSProfile]:
default_profiles = [
DNSProfile("Cloudflare", [DNSServer("Cloudflare", "1.1.1.1", "1.0.0.1")]),
DNSProfile("Cloudflare DoH", [DNSServer("Cloudflare DoH", "1.1.1.1", "1.0.0.1", DNSType.DOH, "https://cloudflare-dns.com/dns-query")]),
DNSProfile("Google", [DNSServer("Google", "8.8.8.8", "8.8.4.4")]),
DNSProfile("Quad9", [DNSServer("Quad9", "9.9.9.9", "149.112.112.112")]),
DNSProfile("OpenDNS", [DNSServer("OpenDNS", "208.67.222.222", "208.67.220.220")])
]
if not self.config_file.exists():
self.save_profiles(default_profiles)
return default_profiles
try:
with self.config_file.open('r') as f:
return self._deserialize_profiles(json.load(f))
except (json.JSONDecodeError, TypeError):
return default_profiles
def save_profiles(self, profiles: List[DNSProfile]):
try:
with self.config_file.open('w') as f:
json.dump(self._serialize_profiles(profiles), f, indent=4)
except Exception:
pass
def _serialize_profiles(self, profiles: List[DNSProfile]) -> List[dict]:
return [{'name': p.name, 'servers': [s.__dict__ | {'dns_type': s.dns_type.value} for s in p.servers]} for p in profiles]
def _deserialize_profiles(self, data: List[dict]) -> List[DNSProfile]:
return [DNSProfile(item['name'], [DNSServer(**(s | {'dns_type': DNSType(s.get('dns_type', 'standard'))})) for s in item.get('servers', [])]) for item in data]
def add_profile(self, profile: DNSProfile):
self.profiles.append(profile)
self.save_profiles(self.profiles)
def remove_profile(self, profile: DNSProfile):
try:
self.profiles.remove(profile)
self.save_profiles(self.profiles)
except ValueError:
pass
def update_profile(self, old_profile: DNSProfile, new_profile: DNSProfile):
try:
self.profiles[self.profiles.index(old_profile)] = new_profile
self.save_profiles(self.profiles)
except ValueError:
pass

27
src/proxy_manager.py Normal file
View File

@@ -0,0 +1,27 @@
import os
import subprocess
from typing import Dict
from gi.repository import Gio
class ProxyManager:
def __init__(self):
self.desktop_env = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
def get_proxy_settings(self) -> Dict:
settings = {'enabled': False, 'host': '', 'port': 0}
try:
if 'gnome' in self.desktop_env:
gsettings = Gio.Settings.new("org.gnome.system.proxy")
if gsettings.get_string('mode') == 'manual':
settings = {'enabled': True, 'host': gsettings.get_string('http-host'), 'port': gsettings.get_int('http-port')}
elif 'kde' in self.desktop_env:
proxy_type = subprocess.run(['kreadconfig5', '--group', 'Proxy Settings', '--key', 'ProxyType'], capture_output=True, text=True, check=False).stdout.strip()
if proxy_type == '1':
proxy_str = subprocess.run(['kreadconfig5', '--group', 'Proxy Settings', '--key', 'httpProxy'], capture_output=True, text=True, check=False).stdout.strip()
if '://' in proxy_str:
proxy_str = proxy_str.split('://')[1]
host, port = proxy_str.split(':')
settings = {'enabled': True, 'host': host, 'port': int(port)}
except (Exception, FileNotFoundError):
pass
return settings

178
src/pyproject.toml Normal file
View File

@@ -0,0 +1,178 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "namban-dns"
version = "2.0.0"
description = "A modern GTK4/Adwaita DNS management application with DoH support"
authors = [
{name = "Ali Gholipour"},
{name = "Hussein Eslami"},
{name = "Sohrab Behdani"},
{name = "Meshya"},
{name = "ARS101"},
]
license = {text = "GPL-3.0"}
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: GTK",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: System :: Networking",
"Topic :: System :: Systems Administration",
]
keywords = ["dns", "networking", "gtk4", "adwaita", "linux", "systemd"]
dependencies = [
# Core GTK/GI bindings
"PyGObject>=3.46.0",
# System integration
"psutil>=5.9.0",
"systemd-python>=235",
# Configuration and data handling
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
# Network utilities
"dnspython>=2.4.0",
"requests>=2.31.0",
"validators>=0.22.0",
# Logging and monitoring
"structlog>=23.1.0",
"rich>=13.0.0",
# Desktop integration (KDE/GNOME)
"dbus-python>=1.3.2",
"jeepney>=0.8.0", # Alternative D-Bus library
# Configuration file handling
"tomli>=2.0.0; python_version < '3.11'",
"tomli-w>=1.0.0",
# Additional utilities
"click>=8.0.0", # For potential CLI interface
"platformdirs>=3.0.0", # Cross-platform config directories
]
[project.optional-dependencies]
dev = [
# Development tools
"black>=23.0.0",
"isort>=5.12.0",
"flake8>=6.0.0",
"mypy>=1.5.0",
"pytest-cov>=4.1.0",
"pytest-mock>=3.11.0",
# Documentation
"sphinx>=7.0.0",
"sphinx-rtd-theme>=1.3.0",
# Type stubs
"types-requests>=2.31.0",
"types-psutil>=5.9.0",
]
testing = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-mock>=3.11.0",
]
packaging = [
"build>=0.10.0",
"wheel>=0.41.0",
"twine>=4.0.0",
]
[project.urls]
Homepage = "https://github.com/parchlinuxB/namban"
Documentation = "https://github.com/parchlinuxB/namban#readme"
Repository = "https://github.com/parchlinuxB/namban.git"
"Bug Tracker" = "https://github.com/parchlinuxB/namban/issues"
[project.scripts]
namban = "namban.main:main"
namban-cli = "namban.cli:main"
[project.gui-scripts]
namban-gui = "namban.main:main"
[tool.hatch.build.targets.wheel]
packages = ["src/namban"]
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/package",
"/README.md",
"/LICENSE",
"/namban.svg",
]
# Black configuration
[tool.black]
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.venv
| build
| dist
)/
'''
# isort configuration
[tool.isort]
profile = "black"
multi_line_output = 3
line_length = 88
# MyPy configuration
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
# Coverage configuration
[tool.coverage.run]
source = ["src/namban"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

View File

@@ -1,7 +0,0 @@
class DataFile:
def InvalidFile(*a):
...
def InvalidServer(*a):
...
def InvalidProfile(*a):
...

View File

@@ -1,18 +0,0 @@
import sys
DEBUG = '--release' not in sys.argv
_imps = []
def makeSureExists(p):
_imps.append(p)
return p
from pathlib import Path
base = Path(__file__)
PACKAGE_NAME = 'com.parchlinux.namban'
WINDOW_TITLE = 'namban'
APP_DATA_FILE = "./nambanAppdata.json" if DEBUG else "/etc/namban/config.json"
SOCKET_PATH = Path("/tmp/namban.sock")
APP_LOCK_PATH = Path("/tmp/namban.lck")
APP_FILES_PATH = makeSureExists(Path("/usr/share/namban"))
OLD_CONFS_PATH = makeSureExists(APP_FILES_PATH/'oldconffiles')
PROMISES_PATH = makeSureExists(APP_FILES_PATH/'promises')

View File

@@ -1,8 +0,0 @@
import checks
def check():
checks.checkPaths()
checks.checkPromises()
if __name__ == "__main__":
check()

View File

@@ -1,74 +0,0 @@
import domain
import settings
import report
import os
import json
class storage:
def loadProf(self, rawProf):
try:
profWargs = {
"server1" : self.loadServ(rawProf['server1']),
"name": rawProf['name']
}
if "server2" in rawProf:
profWargs['server2'] = self.loadServ(rawProf['server2'])
return(
domain.profile(**profWargs)
)
except:
report.DataFile.InvalidProfile()
def loadServ(self,rawServ)->domain.server:
return domain.server(
**rawServ
)
def read(self):
emptyConf = domain.appData([])
if not os.path.exists(settings.APP_DATA_FILE):
report.DataFile.InvalidFile()
return emptyConf
with open(settings.APP_DATA_FILE,"r") as f:
try:
rawData = json.loads(
f.read()
)
except:
report.DataFile.InvalidFile()
return emptyConf
profiles = []
for rawProf in rawData.get('profiles',[]):
r = self.loadProf(rawProf)
if r:
profiles.append(r)
return domain.appData(
profiles
)
def write(self,appData:domain.appData):
rawData = {
"profiles":[
self.dumpProf(p) for p in appData.profiles
]
}
try:
with open(settings.APP_DATA_FILE,"w+") as f:
f.write(
json.dumps(rawData, indent=4)
)
except:
report.DataFile.InvalidFile()
def dumpServ(self, serv:domain.server) -> dict:
return {
'type' : serv.type,
'url' : serv.url
}
def dumpProf(self, prof:domain.profile) -> dict:
rawProf = {
"server1":self.dumpServ(prof.server1),
"name":prof.name
}
if prof.server2:
rawProf['server2'] = self.dumpServ(prof.server2)
return rawProf

103
src/ui_components.py Normal file
View File

@@ -0,0 +1,103 @@
import gi
from typing import Optional, Callable
from gi.repository import Gtk, Adw
from models import DNSProfile, DNSServer, DNSType
from proxy_manager import ProxyManager
class SettingsPage(Adw.PreferencesPage):
def __init__(self):
super().__init__()
self.proxy_manager = ProxyManager()
self._build_ui()
self._load_settings()
def _build_ui(self):
self.set_title("Settings")
group = Adw.PreferencesGroup(title="Legacy Proxy Configuration", description="Reads system-wide proxy settings. This feature is read-only.")
self.add(group)
self.host_row = Adw.EntryRow(title="Proxy Host", editable=False)
self.port_row = Adw.EntryRow(title="Proxy Port", editable=False)
group.add(self.host_row)
group.add(self.port_row)
def _load_settings(self):
settings = self.proxy_manager.get_proxy_settings()
if settings['enabled']:
self.host_row.set_text(settings['host'])
self.port_row.set_text(str(settings['port']))
else:
self.host_row.set_text("Proxy is disabled or not configured")
self.port_row.set_text("")
class ProfileRow(Adw.ActionRow):
def __init__(self, profile: DNSProfile, on_activate: Callable, on_edit: Callable, on_delete: Callable):
super().__init__()
self.profile = profile
self.set_title(profile.name)
self.set_subtitle("".join([s.primary + (f", {s.secondary}" if s.secondary else "") for s in profile.servers]))
self.switch = Gtk.Switch(valign=Gtk.Align.CENTER)
self.switch.connect('state-set', lambda _, state: on_activate(self.profile, state))
self.add_suffix(self.switch)
edit_button = Gtk.Button(icon_name="edit-symbolic", valign=Gtk.Align.CENTER, css_classes=["flat"])
edit_button.connect('clicked', lambda _: on_edit(self.profile))
self.add_suffix(edit_button)
delete_button = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER, css_classes=["flat", "error"])
delete_button.connect('clicked', lambda _: on_delete(self.profile))
self.add_suffix(delete_button)
def set_active(self, active: bool):
with self.switch.freeze_notify():
self.switch.set_active(active)
class ProfileEditSheet(Gtk.Box):
def __init__(self, profile: Optional[DNSProfile] = None):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.profile = profile
self._build_form()
if profile:
self._populate_fields()
def _build_form(self):
clamp = Adw.Clamp(maximum_size=400, tightening_threshold=300)
self.append(clamp)
page = Adw.PreferencesPage(margin_top=12, margin_bottom=12, margin_start=12, margin_end=12)
clamp.set_child(page)
group = Adw.PreferencesGroup()
page.add(group)
self.name_row = Adw.EntryRow(title="Profile Name")
self.primary_row = Adw.EntryRow(title="Primary DNS")
self.secondary_row = Adw.EntryRow(title="Secondary DNS (Optional)")
self.description_row = Adw.EntryRow(title="Description (Optional)")
type_model = Gtk.StringList.new(["Standard DNS", "DNS over HTTPS (DoH)", "DNS over TLS (DoT)"])
self.type_row = Adw.ComboRow(title="DNS Type", model=type_model)
self.type_row.connect('notify::selected', lambda *_: self.doh_row.set_visible(self.type_row.get_selected() == 1))
self.doh_row = Adw.EntryRow(title="DoH URL", visible=False)
for row in [self.name_row, self.type_row, self.primary_row, self.secondary_row, self.doh_row, self.description_row]:
group.add(row)
def _populate_fields(self):
self.name_row.set_text(self.profile.name)
if self.profile.servers:
s = self.profile.servers[0]
self.primary_row.set_text(s.primary)
self.secondary_row.set_text(s.secondary or "")
self.description_row.set_text(s.description or "")
self.doh_row.set_text(s.doh_url or "")
self.type_row.set_selected({DNSType.STANDARD: 0, DNSType.DOH: 1, DNSType.DOT: 2}.get(s.dns_type, 0))
def get_profile(self) -> Optional[DNSProfile]:
name = self.name_row.get_text().strip()
primary = self.primary_row.get_text().strip()
if not name or not primary:
return None
dns_type = [DNSType.STANDARD, DNSType.DOH, DNSType.DOT][self.type_row.get_selected()]
server = DNSServer(name=name, primary=primary,
secondary=self.secondary_row.get_text().strip() or None,
description=self.description_row.get_text().strip() or None,
dns_type=dns_type,
doh_url=self.doh_row.get_text().strip() if dns_type == DNSType.DOH else None)
return DNSProfile(name, [server])

View File

@@ -1,443 +0,0 @@
# In the name of god
from gi.repository import Gdk, Gtk
import settings
import domain as domain
from pathlib import Path
from .textCheck import isIp,baseCheck
class trashIcon(Gtk.Image):
def __init__(self):
super().__init__()
self.set_from_icon_name("trash-empty")
# self.set_size_request(15,15)
class pencilIcon(Gtk.Image):
def __init__(self):
super().__init__()
self.set_from_icon_name("draw-freehand")
class profileConnectButton(Gtk.Switch):
def __init__(self, app:domain.app, profile:domain.profile, default=False):
super().__init__()
self.app = app
self.domain = profile
self.add_css_class('server-button')
self.set_state(default)
self.connect("state-set", self.onChange)
def onChange(self, _, state):
if state:
self.app.connectProfile(self.domain)
else:
self.app.disconnectProfiles()
class Label(Gtk.Label):
def __init__(self,label,classes=[],maxChar=None,**more):
if maxChar:
if label.__len__() > maxChar:
if maxChar > 2 :
label = label[:maxChar-2] + ".."
else :
label = label[:maxChar]
super().__init__(label=label,**more)
self.set_css_classes(classes+['label'])
class LeftFloat(Gtk.AspectFrame):
def __init__(self,widget):
super().__init__(
halign=Gtk.Align.START,
obey_child=True
)
self.set_child(widget)
class centerFloat(Gtk.AspectFrame):
def __init__(self,widget):
super().__init__(
halign=Gtk.Align.CENTER,
xalign=0.5,
yalign=0.5,
# ratio=1.0,
obey_child=True
)
self.set_child(widget)
class Fixed(Gtk.Fixed):
def __init__(self,classes=[]):
super().__init__()
self.set_css_classes(classes)
def put(self, widget, x, y):
super().put(widget, x, y)
return self
class profileInformation(Gtk.Fixed):
def __init__(self,profile:domain.profile):
super().__init__()
self.prof = profile
self.update()
self.add_css_class('server-information')
def update(self):
try:
self.remove(self.title)
self.title = None
self.remove(self.inform)
self.inform = None
except:...
self.title = Label(
self.prof.name,
classes=[
'color-high',
'font-l'
],
maxChar=11
)
serversAspectFrame = Gtk.AspectFrame()
serversAspectFrame.add_css_class('server-aspect-frame')
self.servers = Grid(
classes=[
'ml-abit'
]
)
serversAspectFrame.set_child(
self.servers
)
servers = [self.prof.server1]
if self.prof.server2: servers.append(self.prof.server2)
for i, serv in enumerate(servers):
self.servers.attach(
LeftFloat(
Label(
f'{i+1}: {str(serv)}',
classes=['font-s','color-low'],
maxChar=11
)
),0,i,1,1
)
self.put(
self.title,
0,5
)
self.put(
serversAspectFrame,
80,0
)
class Button(Gtk.Button):
def __init__(self, child, onClick=lambda*_:... , classes=[],size=None,enable=True):
super().__init__()
self.set_child(child)
self.set_css_classes(['button'] + classes)
# self.set_relief(Gtk.ReliefStyle.NONE)
if size:
self.set_size_request(*size)
self.set_hexpand(False)
self.set_vexpand(False)
self.connect('clicked', onClick)
self.set_sensitive(enable)
def enable_css(self, className):
if className not in self.get_css_classes():
self.add_css_class(className)
def disable_css(self, className):
while className in self.get_css_classes():
self.remove_css_class(className)
def enable(self):
self.set_sensitive(True)
def disable(self):
self.set_sensitive(False)
class profile(Gtk.Box):
def __init__(self,app:domain.app,profile:domain.profile):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.add_css_class('server')
self.fixed = Gtk.Fixed()
self.app = app
self.domain = profile
self.append(
self.fixed
)
self.connectButton = profileConnectButton(app,profile)
self.fixed.put(
self.connectButton,
235,4
)
self.fixed.put(
Button(
trashIcon(),lambda *_:self.app.deleteProfile(profile),[]
)
,
189,0
)
self.fixed.put(
Button(
pencilIcon(),lambda *_ : self.app.window.goEdit(profile,lambda newp : self.app.editProfile(profile, self.app.window.editor.dumpProfToInput(newp)) ),[]
)
,
150,0
)
self.fixed.put(
profileInformation(profile),
10,0
)
self.update()
def update(self):
self.connectButton.set_state(
self.app.connectedProfile == self.domain
)
class Grid(Gtk.Grid):
def __init__(self,*args,classes=[],**wargs):
super().__init__(*args,**wargs)
self.set_css_classes(classes)
def empty (self):
children = self.get_children()
for child in children:
self.remove(child)
class profileList(Grid):
def __init__(self, app:domain.app):
super().__init__()
self.add_css_class('server-list')
self.app = app
self.profs = []
self.start()
def start(self):
for i, p in enumerate(self.app.data.profiles):
prof = profile(self.app,p)
self.profs.append(prof)
self.attach(
prof,
0,i,1,1
)
def restart(self):
self.remove_column(0)
self.profs = []
self.start()
def update(self):
for current in self.profs:
if current.domain not in self.app.data.profiles:
self.restart()
for featue in self.app.data.profiles:
if featue not in list(map(lambda x:x.domain, self.profs)):
self.restart()
for ch in self.profs:
ch.update()
class addButton(Gtk.Overlay):
def __init__(self, app):
super().__init__()
self.app = app
self.button = Button(Label('+'),self.click)
self.button.set_css_classes(['add-btn'])
self.set_child(self.button)
def click(self, *_):
self.app.window.goEdit(None, lambda data: self.app.addProfile(
domain.profile(
server1=domain.server(url=data['server1'],type='dns'),
server2=domain.server(url=data['server2'],type='dns') if data.get('server2',False) else None,
name=data['name']
)
))
class mainWindowContainer(Gtk.Fixed):
def __init__(self, app):
super().__init__()
self.childs = [profileList(app)]
self.put(
self.childs[0],0,0
)
self.put(
addButton(app),10,350
)
def update(self):
for ch in self.childs:
ch.update()
class Input(Gtk.Entry):
def __init__(self,app=None,name="",optional=False,placeholder=None,classes=[],onchange=None,checker=lambda *_:True):
super().__init__()
self.set_size_request(250,20)
self.app = app
self.set_css_classes(['input'] + classes)
self.name = name
self.optional = optional
if placeholder:
self.set_placeholder_text(placeholder)
if onchange:
self.connect("changed", onchange)
self.checker = checker
class Form:
def __init__(self, *inputs:list[Input]) -> None:
self.inputs = inputs
class LowEntryError(Exception):
def __init__(self, *entries):
super().__init__()
self.inputs = entries
def read(self):
empties = []
for i in self.inputs:
i:Input
t:str = i.get_text()
if t.strip() == '':
if not i.optional:
empties.append(i)
else:
if not i.checker(t):
empties.append(i)
if empties:
raise self.LowEntryError(*empties)
return {
i.name: i.get_text() for i in self.inputs
}
def write(self, data:dict):
for inp in self.inputs:
try:
if not data:
inp.set_text("")
elif inp.name in data:
inp.set_text(data[inp.name])
except:...
class editWindowContainer(Gtk.Box):
def handleOK (self, *args):
# self.then(self.data)
self.app.window.goMain()
def set_entries(self, baseData ,then):
self.form.write(baseData)
self.then = then
def __init__(self,app):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.app = app
self.inputs = [
Input(self.app, "name", onchange=self.update, classes=['mt-6'], placeholder="Name"),
Input(self.app, "server1",onchange=self.update, classes=['mt-2'], checker=isIp(),placeholder="Server 1"),
Input(self.app, "server2",onchange=self.update, classes=['mt-2'], checker=isIp(),placeholder="Server 2 (optional)", optional=True)
]
self.form = Form(*self.inputs)
for i in self.inputs : self.append(centerFloat(i))
self.okbtn = Button(Label("Ok"),self.ok, classes=['mb-1'], enable=False)
self.canbtn = Button(Label("Cancel"),self.cancel, classes=['mb-1', 'bg-red'])
self.append(
centerFloat(
Fixed(classes=['mt-3'])
.put(
self.okbtn, 10, 0
)
.put(
self.canbtn, 60, 0
)
)
)
def update(self,*_):
try:
data = self.form.read()
self.okbtn.enable()
self.okbtn.enable_css("bg-green")
except Form.LowEntryError as e:
self.okbtn.disable()
self.okbtn.disable_css("bg-green")
def ok(self,*_):
try:
data = self.form.read()
if self.then:
self.then(data)
except:
...
self.set_entries(None, None)
self.back()
def cancel(self,*_):
self.set_entries(None, None)
self.back()
def back(self):
self.app.window.goMain()
def loadProfFromInput(self, prof):
r = {
"name":prof.name,
"server1":prof.server1.url
}
if prof.server2:
r['server2'] = prof.server2.url
return r
def dumpProfToInput(self,prof):
wargs = {
"name":prof['name'],
"server1":self.app.loadServ({'url':prof['server1'],'type':'dns'})
}
if prof.get('server2',False).strip():
wargs['server2'] = self.app.loadServ({'url':prof['server2'],'type':'dns'})
return domain.profile(**wargs)
class Stack(Gtk.Stack):
def __init__(self,**data):
super().__init__()
self.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
self.data = data
self.add(**data)
def show(self,name):
self.set_visible_child(
self.data[name]
)
def add(self,**data):
self.data = {
**self.data,
**data
}
for _, widget in data.items():
Gtk.Stack.add_child(self,widget)
import time
class window(Gtk.ApplicationWindow):
def __init__(self, app=None):
while not Gtk.init_check():
time.sleep(0.1)
super().__init__(application=app, title=settings.WINDOW_TITLE)
app.window = self
self.app = app
self.set_default_size(300,400)
self.main = mainWindowContainer(app)
self.editor = editWindowContainer(app)
self.stack = Stack(
main = self.main,
editor = self.editor
)
self.set_child(
self.stack
)
cssProvider = Gtk.CssProvider()
cssProvider.load_from_path(
str(Path(
__file__
).parent / "style.css"
)
)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
cssProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.goMain()
def update(self):
self.main.update()
def goMain(self):
self.update()
self.stack.show('main')
def goEdit(self, baseData=None, then=None):
if isinstance(baseData,domain.profile):
baseData = self.editor.loadProfFromInput(baseData)
self.editor.set_entries(baseData, then)
self.stack.show('editor')
__all__ = [
window
]

View File

@@ -1,91 +0,0 @@
.server{
border-radius: 10px;
border: 1px solid #666 ;
color: aqua;
margin-top: 3px;
padding-top: 5px;
padding-bottom: 5px;
}
.color-aqua{
color: #d54;
}
.font-l{
font-size: 16px;
}
.font-s{
font-size: 11px;
}
.color-high{
color:#ccc;
}
.color-low{
color: #666;
}
.server-button{
margin-right: 5px;
}
.server-list{
margin-left: 4px;
}
.input{
border-radius: 10px;
}
.server-informaion{
}
.add-btn{
font-size: 20px;
border-radius: 15px;
min-width: 20px;
min-height: 30px;
}
.server-aspect-frame{
min-height: 34px;
}
.trash-btn{
}
.button{
border: none;
border-radius: 12px;
}
.text-lg{
font: 16px bolder;
}
.titlebar{
/* background-color: aqua; */
}
.mt-1{
margin-top: 5px;
}
.mt-2{
margin-top: 10px;
}
.mt-3{
margin-top: 15px;
}
.mt-6{
margin-top: 30px;
}
.mb-1{
margin-bottom: 5px;
}
.ml-abit{
margin-left: 3px;
}
.bg-red{
/* background-color: #d54; */
background: rgb(250, 20, 20);
color: azure;
}
.bg-red:hover{
background: rgb(250, 100, 100);
}
.bg-green{
background: rgba(53, 167, 8, 0.993);
color: azure;
}
.bg-green:hover{
background: rgb(125, 255, 73);
}

View File

@@ -1,16 +0,0 @@
class baseCheck:
def check(self,text):
return False
def __call__(self, text):
return self.check(text)
class regexCheck(baseCheck):
pattern = ""
def check(self,check):
import re
if re.match(self.pattern, check):
return True
return False
class isIp(regexCheck):
pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"