Skip to content

Architecture

AmooReza edited this page Oct 9, 2026 · 1 revision

πŸ— Architecture

A technical overview of the GoodByeDPI GUI codebase β€” for contributors and anyone curious about how it's built.


🎯 Design Principles

The codebase follows three simple rules:

Principle Why
UI is pure Widgets only emit signals. They never perform business logic.
Core is testable No direct dependency on widgets. Logic can be unit-tested in isolation.
Controller wires everything A single object connects UI signals to business logic to side-effects.

This separation means you can redesign the UI without touching business logic β€” and vice versa.


πŸ“ Project Layout

GoodByeDPI-GUI/
β”œβ”€β”€ main.py                     ← entry point
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ README.md
β”œβ”€β”€ LICENSE
β”œβ”€β”€ .gitignore
β”‚
β”œβ”€β”€ assets/                     ← visual resources
β”‚   β”œβ”€β”€ logo.ico
β”‚   └── Screenshot.png
β”‚
β”œβ”€β”€ bin/                        ← runtime binaries & drivers
β”‚   β”œβ”€β”€ goodbyedpi.exe
β”‚   β”œβ”€β”€ WinDivert.dll
β”‚   └── WinDivert64.sys
β”‚
└── app/                        ← application package
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ constants.py            ← all app constants & mode presets
    β”œβ”€β”€ settings.py             ← QSettings wrapper
    β”œβ”€β”€ utils.py                ← helpers (paths, admin check, versions)
    β”œβ”€β”€ validator.py            ← custom-arguments validator
    β”‚
    β”œβ”€β”€ core/                   ← business logic
    β”‚   β”œβ”€β”€ __init__.py
    β”‚   β”œβ”€β”€ controller.py       ← wires UI, tray, settings, updater
    β”‚   └── updater.py          ← GitHub release checker (QThread)
    β”‚
    └── ui/                     ← presentation layer
        β”œβ”€β”€ __init__.py
        β”œβ”€β”€ main_window.py      ← MainWindow (signals only)
        β”œβ”€β”€ dialogs.py          ← UpdateDialog, AboutDialog, Args Guide
        β”œβ”€β”€ widgets.py          ← StatusDot, StatusPill, UptimeChip,
        β”‚                          ModeCard, ModeCardGroup,
        β”‚                          Toast, ToastManager
        β”œβ”€β”€ theme.py            ← Catppuccin-inspired QSS
        └── tray.py             ← TrayManager (system tray)

🧩 Module Responsibilities

πŸ“ main.py β€” Entry Point

  • Creates QApplication
  • Checks admin privileges β€” refuses to start if not elevated
  • Instantiates AppController and shows the window
  • Runs the Qt event loop

πŸ“Œ app/constants.py

  • App name, version, author
  • GitHub repository info & release URL builder
  • Mode presets as NamedTuple (key, title, badge, icon, description, args, tooltip)
  • Bundled resource paths (DPI_EXE_REL, LOGO_ICO_REL)

βš™οΈ app/settings.py

  • Thin wrapper around QSettings
  • Properties: mode_index, custom_args, geometry
  • Uses Windows registry under the hood

πŸ›  app/utils.py

  • resource_path() β€” resolves bundled paths for both dev & PyInstaller
  • is_admin() β€” checks via ctypes.windll.shell32.IsUserAnAdmin
  • executable_path() β€” returns EXE path or main.py in dev
  • parse_version(), is_newer_version()

βœ… app/validator.py

  • Pure function validate_args(args_str)
  • Returns ValidationResult with errors & warnings
  • No Qt imports β€” fully testable standalone
  • Knows every GoodbyeDPI flag and cross-flag rules

🧠 Core Layer β€” app/core/

πŸŽ› controller.py β€” AppController

The heart of the app. Wires everything together:

  • Owns MainWindow, TrayManager, UpdateChecker
  • Manages QProcess for launching goodbyedpi.exe
  • Persists state to AppSettings
  • Handles autostart via Windows Task Scheduler
  • Displays dialogs (About, Update, Custom Args Help)

πŸ”„ updater.py

Two classes:

  • _UpdateWorker(QThread) β€” scrapes the /releases/latest redirect (GitHub API is avoided because it's blocked in some regions)
  • UpdateChecker(QObject) β€” public API with three signals: update_available(str), up_to_date(), error()

πŸ’‘ Why scrape instead of using the API? GitHub's api.github.com is blocked in certain countries. Following the redirect of /releases/latest gives us the version number using a domain that's more likely to be reachable.


🎨 UI Layer β€” app/ui/

πŸ–Ό main_window.py β€” MainWindow

  • Pure presentation β€” only emits signals
  • Signals: start_clicked, stop_clicked, mode_changed(int), custom_args_changed(str), etc.
  • Contains zero business logic β€” the controller decides what happens
  • Manages the ToastManager overlay
  • Handles smooth window resize when switching modes
  • Applies Windows dark title bar via DwmSetWindowAttribute

πŸ“¦ widgets.py

Custom, reusable widgets:

Widget Purpose
StatusDot Animated colored dot (pulses when running)
StatusPill Pill combining dot + label
UptimeChip Live HH:MM:SS session timer
ModeCard Single selectable mode card
ModeCardGroup Radio-style group of ModeCards
Toast Self-dismissing notification card
ToastManager Stacks & positions toasts

πŸ’¬ dialogs.py

  • UpdateDialog β€” shows current vs. latest version cards
  • AboutDialog β€” app info, powered-by links, credits
  • CustomArgsHelpDialog β€” tabbed flag guide with live search

🎨 theme.py

  • Single global DARK_QSS string
  • Catppuccin-inspired palette
  • Applied to MainWindow; child widgets inherit

πŸ”” tray.py β€” TrayManager

  • Owns QSystemTrayIcon and its menu
  • Signals: start_requested, stop_requested, exit_requested, restore_requested
  • Updates icon tooltip and menu state based on running status

πŸ”— Signal Flow

Here's how a user click travels through the app:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User clicks "β–Ά Start"                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  MainWindow.start_clicked  (Signal)                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  AppController.start()  (Slot)                               β”‚
β”‚                                                              β”‚
β”‚  1. Reads current mode from window                           β”‚
β”‚  2. Validates custom args (if Custom mode)                   β”‚
β”‚  3. Checks DPI_EXE exists                                    β”‚
β”‚  4. Creates QProcess                                         β”‚
β”‚  5. Emits updates β†’ window.set_running_state(True)           β”‚
β”‚  6. Emits updates β†’ tray.set_running(True)                   β”‚
β”‚  7. Appends log separator                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The UI never decides what to do β€” it just says "the user clicked Start" and the controller figures out the rest.


🎬 Process Lifecycle

Controller.start()
    β”‚
    β–Ό
QProcess.start(goodbyedpi.exe, args)
    β”‚
    β”œβ”€β”€β”€ readyReadStandardOutput ───→ window.append_log(text)
    β”‚
    β”œβ”€β”€β”€ finished(exit_code) ───────→ _on_process_finished()
    β”‚                                     β”‚
    β”‚                                     β”œβ”€β†’ window.set_running_state(False)
    β”‚                                     β”œβ”€β†’ tray.set_running(False)
    β”‚                                     └─→ process.deleteLater()
    β”‚
    └─── errorOccurred(error) ──────→ _on_process_error()
                                          β”‚
                                          └─→ window.append_log("[error] ...")

Controller.stop()
    β”‚
    β–Ό
QProcess.kill() + waitForFinished(2000)
    β”‚
    β–Ό
finished signal fires β†’ same path as natural exit

πŸ§ͺ Adding a New Feature

Say you want to add a "Restart" button next to Start/Stop. Here's the workflow:

1. Add UI

In app/ui/main_window.py:

  • Add restart_clicked = Signal()
  • Create the button, add to layout
  • Wire: self.restart_btn.clicked.connect(self.restart_clicked.emit)

2. Add Logic

In app/core/controller.py:

  • Connect: self.window.restart_clicked.connect(self.restart)
  • Implement restart(self) slot: call self.stop() then self.start()

3. Style It

In app/ui/theme.py:

  • Add QSS for QPushButton#restartBtn

No UI code needs to know how restart works. No core code needs to know the button exists.


πŸ§ͺ Testing Strategy

Layer How to Test
validator.py Unit tests β€” pure function, no Qt needed
utils.py Unit tests β€” version parsing, path resolution
core/ Integration β€” mock the UI, verify signal wiring
ui/ Manual β€” run python main.py and interact

Because the UI and core are decoupled, you can test business logic without ever showing a window.


🎨 Theme System

The entire visual style is one QSS string in app/ui/theme.py:

DARK_QSS = """
QMainWindow { background: ...; }
QPushButton#startBtn { background-color: #a6e3a1; ... }
QFrame#modeCard[selected="true"] { border: 1px solid #89b4fa; ... }
..."""

# Applied once at window construction:
self.setStyleSheet(DARK_QSS)

Dynamic states (like a selected mode card) use Qt properties:

# In the widget:
self.setProperty("selected", True)
self.style().unpolish(self)
self.style().polish(self)

QSS then matches via [selected="true"] selector.


πŸ“¦ Bundled Resources

utils.resource_path() handles both development and PyInstaller builds:

def resource_path(relative_path: str) -> str:
    if getattr(sys, "frozen", False):
        base_path = sys._MEIPASS        # PyInstaller temp dir
    else:
        base_path = project_root         # parent of app/
    return os.path.join(base_path, relative_path)

This means resource_path("bin/goodbyedpi.exe") works identically whether you're running from source or from a bundled EXE.


πŸ” Admin Privileges

Three layers enforce admin:

  1. main.py β€” exits with error dialog if not admin
  2. PyInstaller --uac-admin β€” triggers UAC on launch
  3. Task Scheduler β€” created with /rl highest for autostart

🧭 Where to Go Next

I want to... Look at
Change the visual style app/ui/theme.py
Add a button app/ui/main_window.py + app/core/controller.py
Add a GoodbyeDPI flag app/validator.py + app/ui/dialogs.py
Tweak update behavior app/core/updater.py
Add a setting app/settings.py
Add a mode preset app/constants.py (append to MODE_PRESETS)

🀝 Contributing Code

  1. Fork the repo
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Follow the UI ↔ Core separation β€” no business logic in widgets
  4. Comment non-obvious code
  5. Test manually with python main.py
  6. Commit: git commit -m "feat: add amazing feature"
  7. Push and open a Pull Request

πŸ’‘ Prefer Conventional Commits: feat:, fix:, refactor:, docs:, style:.


Ready to build? See Build from Source πŸ› 

Happy hacking πŸ§‘β€πŸ’»

πŸš€ GoodByeDPI GUI


πŸ“– Documentation

🩺 Support

πŸ›  Development


πŸ”— Links


Made with ❀️ by AmooReza

Clone this wiki locally