Architecture - ZvanTors/GoodByeDPI-GUI GitHub Wiki
A technical overview of the GoodByeDPI GUI codebase โ for contributors and anyone curious about how it's built.
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.
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)
- Creates
QApplication - Checks admin privileges โ refuses to start if not elevated
- Instantiates
AppControllerand shows the window - Runs the Qt event loop
- 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)
- Thin wrapper around
QSettings - Properties:
mode_index,custom_args,geometry - Uses Windows registry under the hood
-
resource_path()โ resolves bundled paths for both dev & PyInstaller -
is_admin()โ checks viactypes.windll.shell32.IsUserAnAdmin -
executable_path()โ returns EXE path ormain.pyin dev -
parse_version(),is_newer_version()
- Pure function
validate_args(args_str) - Returns
ValidationResultwitherrors&warnings - No Qt imports โ fully testable standalone
- Knows every GoodbyeDPI flag and cross-flag rules
The heart of the app. Wires everything together:
- Owns
MainWindow,TrayManager,UpdateChecker - Manages
QProcessfor launchinggoodbyedpi.exe - Persists state to
AppSettings - Handles autostart via Windows Task Scheduler
- Displays dialogs (About, Update, Custom Args Help)
Two classes:
-
_UpdateWorker(QThread)โ scrapes the/releases/latestredirect (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.comis blocked in certain countries. Following the redirect of/releases/latestgives us the version number using a domain that's more likely to be reachable.
- 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
ToastManageroverlay - Handles smooth window resize when switching modes
- Applies Windows dark title bar via
DwmSetWindowAttribute
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 |
-
UpdateDialogโ shows current vs. latest version cards -
AboutDialogโ app info, powered-by links, credits -
CustomArgsHelpDialogโ tabbed flag guide with live search
- Single global
DARK_QSSstring - Catppuccin-inspired palette
- Applied to
MainWindow; child widgets inherit
- Owns
QSystemTrayIconand its menu - Signals:
start_requested,stop_requested,exit_requested,restore_requested - Updates icon tooltip and menu state based on running status
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.
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 exitSay you want to add a "Restart" button next to Start/Stop. Here's the workflow:
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)
In app/core/controller.py:
- Connect:
self.window.restart_clicked.connect(self.restart) - Implement
restart(self)slot: callself.stop()thenself.start()
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.
| 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.
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.
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.
Three layers enforce admin:
-
main.pyโ exits with error dialog if not admin -
PyInstaller
--uac-adminโ triggers UAC on launch -
Task Scheduler โ created with
/rl highestfor autostart
| 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) |
- Fork the repo
- Create a feature branch:
git checkout -b feature/amazing-feature - Follow the UI โ Core separation โ no business logic in widgets
- Comment non-obvious code
- Test manually with
python main.py - Commit:
git commit -m "feat: add amazing feature" - Push and open a Pull Request
๐ก Prefer Conventional Commits:
feat:,fix:,refactor:,docs:,style:.
Ready to build? See Build from Source ๐
Happy hacking ๐งโ๐ป