madOS Installer is a GTK3-based Linux distribution installer written in Python. It provides a graphical wizard interface for installing madOS (an Arch Linux derivative) with Sway/Hyprland desktop environment.
mados-installer/
├── app.py # Main window and orchestration
├── config.py # Configuration constants
├── utils.py # Shared utility functions
├── theme.py # GTK theme application
├── colors.py # Nord color palette definitions
├── css.py # CSS styles for GTK
├── translations.py # Translation helper
├── pages/ # Installer wizard pages
│ ├── base.py # Shared UI helpers
│ ├── welcome.py
│ ├── disk.py
│ ├── partitioning.py
│ ├── user.py
│ ├── locale.py
│ ├── summary.py
│ ├── installation.py
│ └── completion.py
├── installer/ # Installation logic
│ ├── config_script.py # Bash script generator
│ └── steps.py
├── translations/ # Language translations
│ ├── en.py, es.py, fr.py, de.py, etc.
└── tests/ # Test suite
└── test_config_script.py
Run all tests:
python3 -m unittest discover -s tests -vRun a single test:
python3 -m unittest tests.test_config_script.TestConfigScript.test_basic_replacements -vOr using pytest (if installed):
pytest tests/test_config_script.py::TestConfigScript::test_basic_replacements -vRun in demo mode (no system changes):
python3 -m appRun in demo mode (explicit):
DEMO_MODE=true python3 -m appOr run the executable wrapper:
./mados-installer- Use Python 3 with type hints where beneficial
- Follow PEP 8 style guidelines
- Use 4 spaces for indentation (not tabs)
- Maximum line length: 120 characters
- Use meaningful, descriptive names
Organize imports in the following order with blank lines between groups:
# Standard library
import os
import sys
import re
# Third-party libraries
import gi
from gi.repository import Gtk, GLib, GdkPixbuf
# Local application imports
from config import DEMO_MODE, LOCALE_MAP
from translations import TRANSLATIONS
from utils import random_suffix, show_error- Functions/variables:
snake_case(e.g.,random_suffix,install_data) - Classes:
PascalCase(e.g.,MadOSInstaller,TestConfigScript) - Constants:
UPPER_SNAKE_CASE(e.g.,DEMO_MODE,MIN_DISK_SIZE_GB) - Private methods: prefix with underscore (e.g.,
_build_pages,_escape_shell)
Use type hints for function signatures, especially for public APIs:
def build_config_script(data: dict) -> str:
"""Build the chroot configuration shell script."""
...
def create_page_header(app, title: str, step_num: int, total_steps: int = 7):
"""Create consistent page header with step indicator dots."""
...Use docstrings for all public functions and classes:
def random_suffix(length=4):
"""Generate random hostname suffix"""
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
class MadOSInstaller(Gtk.Window):
"""Main installer window — orchestrates pages and holds shared state."""- Use specific exception types (
ValueErrorfor validation,OSErrorfor I/O) - Provide clear error messages that explain what went wrong
- Validate inputs at function entry points
def build_config_script(data):
disk = data["disk"]
timezone = data["timezone"]
if timezone not in TIMEZONES:
raise ValueError(f"Invalid timezone: {timezone}")
username = data["username"]
if not re.match(r"^[a-z_][a-z0-9_-]*$", username):
raise ValueError(f"Invalid username: {username}")- Use
Gtk.Orientation.VERTICAL/Gtk.Orientation.HORIZONTALinstead of strings - Use
Gtk.Align.CENTER,Gtk.Align.START, etc. for alignment - Apply dark theme using CSS classes and context add/remove
- Use
GLib.idle_add()for thread-safe UI updates
# Good
header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
header.get_style_context().add_class("page-header")
title_label.set_halign(Gtk.Align.CENTER)
# Thread-safe logging
GLib.idle_add(_log_idle, app, message)When generating shell scripts (like config_script.py):
- Use single quotes for strings that shouldn't expand (
'${var}') - Escape braces for Python f-strings:
{{ echo; }}becomes{ echo; } - Use the
_escape_shell()helper for user-provided strings - Include progress markers:
[PROGRESS 1/8],[PROGRESS 2/8], etc.
return f'''#!/bin/bash
set -e
echo "[PROGRESS 1/8] Setting timezone and locale..."
# Timezone
ln -sf /usr/share/zoneinfo/{timezone} /etc/localtime
...
'''- Use Python's built-in
unittestmodule - Name test files as
test_*.py - Use descriptive test method names:
test_*_replacements,test_*_not_* - Test both success and failure cases (invalid inputs)
class TestConfigScript(unittest.TestCase):
def test_basic_replacements(self):
"""Test basic variable replacements work"""
script = build_config_script(self.data)
self.assertIn("/usr/share/zoneinfo/Europe/Madrid", script)
self.assertIn("es_ES.UTF-8 UTF-8", script)
def test_invalid_username(self):
"""Test invalid username raises error"""
data = self.data.copy()
data["username"] = "123invalid"
with self.assertRaises(ValueError):
build_config_script(data)Store all configuration in config.py:
- Package lists (
PACKAGES_PHASE1,PACKAGES_PHASE2) - Locale mappings (
LOCALE_MAP,LOCALE_KB_MAP) - Timezones list (
TIMEZONES) - RSYNC excludes (
RSYNC_EXCLUDES) - Demo mode flag (
DEMO_MODE)
- Store translations in
translations/directory as language modules - Use the translation helper in
app.py:self.t("key") - Access via
TRANSLATIONS[self.current_lang].get(key, key)
Creating a new wizard page:
- Create a function
create_X_page(app)inpages/X.py - Use shared helpers from
pages/base.py:create_page_header(),create_nav_buttons() - Import and add to the page flow in
app.py:_build_pages()
Adding new configuration:
- Add to
config.pyas a constant - Update
install_datadictionary inapp.pyif it's user input - Use in relevant modules via import
- The installer runs as root (required for disk operations)
- Demo mode (
DEMO_MODE=True) allows testing without system changes - The
config_script.pygenerates a bash script that runs in chroot - All UI updates must be thread-safe via
GLib.idle_add()