Architecture - ZvanTors/GoodByeDPI-GUI GitHub Wiki

๐Ÿ— 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 ๐Ÿง‘โ€๐Ÿ’ป

โš ๏ธ **GitHub.com Fallback** โš ๏ธ