diff --git a/gfd/__init__.py b/gfd/__init__.py
index 0e0b578..adc7c6c 100644
--- a/gfd/__init__.py
+++ b/gfd/__init__.py
@@ -1,108 +1 @@
-from PySide6 import QtWidgets, QtCore
-import gfd.i18n as i18n
-from gfd import sfd
-
-
-class GFDWidget(QtWidgets.QWidget):
- def __init__(self, tr):
- super().__init__()
- self.tr = tr
- self.setWindowTitle(self.tr("window_title"))
- self.resize(400, 220)
-
- self.label = QtWidgets.QLabel(
- self.tr("hello_world"),
- alignment=QtCore.Qt.AlignmentFlag.AlignCenter,
- )
-
- self.combo = QtWidgets.QComboBox()
- self.combo.addItem(self.tr("loading_installers") + "…")
-
- self.refresh_btn = QtWidgets.QPushButton("⟳ " + self.tr("refresh"))
- self.refresh_btn.clicked.connect(self.reload_installers)
-
- combo_layout = QtWidgets.QHBoxLayout()
- combo_layout.addWidget(self.combo)
- combo_layout.addWidget(self.refresh_btn)
-
- self.md5_label = QtWidgets.QLabel(
- "", alignment=QtCore.Qt.AlignmentFlag.AlignCenter
- )
-
- layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(self.label)
- layout.addLayout(combo_layout)
- layout.addWidget(self.md5_label)
-
- self.combo.currentIndexChanged.connect(self.on_selection_changed)
-
- QtCore.QTimer.singleShot(0, self.load_installers_async)
-
- def reload_installers(self):
- """Triggered by the refresh button."""
- self.refresh_btn.setEnabled(False)
- self.combo.clear()
- self.combo.addItem(self.tr("loading_installers") + "…")
- self.load_installers_async()
-
- def load_installers_async(self):
- """Fetch installer options without freezing the UI."""
- self.thread = QtCore.QThread()
- self.worker = SFDWorker()
- self.worker.moveToThread(self.thread)
-
- self.thread.started.connect(self.worker.run)
- self.worker.finished.connect(self.thread.quit)
- self.worker.finished.connect(self.worker.deleteLater)
- self.thread.finished.connect(self.thread.deleteLater)
- self.worker.result_ready.connect(self.populate_installers)
-
- self.thread.start()
-
- @QtCore.Slot(list)
- def populate_installers(self, installers):
- self.refresh_btn.setEnabled(True)
- self.combo.clear()
-
- if not installers:
- self.combo.addItem(self.tr("no_installers_found"))
- self.md5_label.setText("")
- return
-
- self.installers = installers
- for item in installers:
- self.combo.addItem(item["name"], item["md5"])
-
- self.combo.setCurrentIndex(0)
- self.on_selection_changed(0)
-
- @QtCore.Slot(int)
- def on_selection_changed(self, index):
- if not hasattr(self, "installers") or not self.installers:
- return
- md5 = self.combo.currentData()
- name = self.combo.currentText()
- self.md5_label.setText(f"{name}
MD5: {md5 or 'N/A'}")
-
-
-class SFDWorker(QtCore.QObject):
- """Background worker to fetch installers safely."""
-
- finished = QtCore.Signal()
- result_ready = QtCore.Signal(list)
-
- @QtCore.Slot()
- def run(self):
- data = sfd.fetchInstallerOptions()
- self.result_ready.emit(data)
- self.finished.emit()
-
-
-def run(lang="es"):
- import sys
-
- app = QtWidgets.QApplication(sys.argv)
- tr = i18n.Translator(lang)
- w = GFDWidget(tr)
- w.show()
- sys.exit(app.exec())
+from .app import run
diff --git a/gfd/app.py b/gfd/app.py
new file mode 100644
index 0000000..2c962b1
--- /dev/null
+++ b/gfd/app.py
@@ -0,0 +1,13 @@
+from PySide6 import QtWidgets
+from gfd.ui.main_window import GFDWidget
+from gfd.i18n import Translator
+
+
+def run(lang="es"):
+ import sys
+
+ app = QtWidgets.QApplication(sys.argv)
+ tr = Translator(lang)
+ window = GFDWidget(tr)
+ window.show()
+ sys.exit(app.exec())
diff --git a/gfd/core/installers.py b/gfd/core/installers.py
new file mode 100644
index 0000000..2fc7d07
--- /dev/null
+++ b/gfd/core/installers.py
@@ -0,0 +1,98 @@
+import time
+import re
+import unicodedata
+from gfd.core import sfd
+
+SUPPORTED_INSTALLERS = [
+ {
+ "os_type": "ubuntu24",
+ "name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
+ "md5": "bdc871e15f2096f930b285f0ed799aa0",
+ },
+ {
+ "os_type": "debian",
+ "name": "Usuarios Linux - Ubuntu 24.04 LTS (DEB 64bits) - 78 MB",
+ "md5": "bdc871e15f2096f930b285f0ed799aa0",
+ },
+]
+
+
+def _normalize(t):
+ return re.sub(r"\s+", " ", unicodedata.normalize("NFKD", t).lower()).strip()
+
+
+def _fetch(max_attempts=3, delay=2):
+ for i in range(max_attempts):
+ data = sfd.fetchInstallerOptions()
+
+ if data:
+ return data
+
+ if i < max_attempts - 1:
+ time.sleep(delay)
+
+ return []
+
+
+def get_available_installers(os_type, installers=None):
+ """
+ Return a list of confirmed available installers for the given OS type.
+
+ If an installer is listed, it means that for a given OS type:
+ - There is a supported installation routine.
+ - There is a downloadable archive from Soporte Firma Digital.
+ """
+ src = installers or SUPPORTED_INSTALLERS
+ locals_ = [i for i in src if i["os_type"] == os_type]
+
+ if not locals_:
+ return []
+
+ remote = _fetch()
+
+ if not remote:
+ return []
+
+ rmap = {_normalize(r["name"]): r.get("md5") for r in remote}
+ confirmed = []
+
+ for local in locals_:
+ lname, lmd5 = (
+ _normalize(local["name"]),
+ (local.get("md5") or "").lower() or None,
+ )
+ rmd5 = rmap.get(lname)
+
+ if (lmd5 == rmd5) or (lmd5 is None and rmd5 is None):
+ confirmed.append((local["name"], lmd5))
+
+ return confirmed
+
+
+# TODO: Routine to check for installed version
+def get_installed_version():
+ """
+ Return the current installed version data, or empty if not installed.
+ """
+ return [] # Return always empty for now
+
+
+if __name__ == "__main__":
+ os_type = "debian"
+ print(f"=== TEST: {os_type.upper()} Installer Validation ===")
+ confirmed = get_available_installers(os_type)
+ print(
+ f"{len(confirmed)} confirmed installer(s):"
+ if confirmed
+ else "No confirmed installers found."
+ )
+
+ for n, m in confirmed:
+ print(f" - {n} (MD5={m or 'N/A'})")
+
+ installed = get_installed_version()
+
+ if not installed:
+ print("Status: NOT INSTALLED")
+ else:
+ print(f"Installed: {installed[0]} MD5={installed[1]}")
diff --git a/gfd/core/osinfo.py b/gfd/core/osinfo.py
new file mode 100644
index 0000000..2025b3b
--- /dev/null
+++ b/gfd/core/osinfo.py
@@ -0,0 +1,74 @@
+import platform
+import re
+from pathlib import Path
+
+
+def _read_os_release():
+ """Read and parse /etc/os-release or /usr/lib/os-release if available."""
+ paths = [Path("/etc/os-release"), Path("/usr/lib/os-release")]
+ data = {}
+
+ for path in paths:
+ if path.exists():
+ try:
+ for line in path.read_text(encoding="utf-8").splitlines():
+ if "=" in line:
+ key, val = line.split("=", 1)
+ data[key.strip()] = val.strip().strip('"')
+ break
+ except Exception:
+ pass
+ return data
+
+
+def get_os_type():
+ """
+ Detect and return a simple OS type string.
+
+ Returns one of:
+ 'macos', 'windows', 'ubuntu24', 'ubuntu22', 'ubuntu20',
+ 'debian', 'arch', 'rpm', or None if unsupported.
+ """
+ system = platform.system().lower()
+ os_type = None
+
+ if system == "darwin":
+ os_type = "macos"
+
+ elif system == "windows":
+ os_type = "windows"
+
+ elif system == "linux":
+ info = _read_os_release()
+ id_name = info.get("ID", "").lower()
+ version_id = info.get("VERSION_ID", "")
+
+ # Ubuntu
+ if id_name == "ubuntu":
+ if version_id.startswith("24"):
+ os_type = "ubuntu24"
+ elif version_id.startswith("22"):
+ os_type = "ubuntu22"
+ elif version_id.startswith("20"):
+ os_type = "ubuntu20"
+
+ # Debian (11 or newer supported)
+ elif id_name == "debian":
+ try:
+ version_num = int(re.findall(r"\d+", version_id or "0")[0])
+ except IndexError:
+ version_num = 0
+ if version_num >= 11:
+ os_type = "debian"
+
+ # Arch / RPM-based
+ elif "arch" in id_name:
+ os_type = "arch"
+ elif id_name in ["fedora", "rhel", "centos", "rocky", "alma", "opensuse"]:
+ os_type = "rpm"
+
+ return os_type
+
+
+if __name__ == "__main__":
+ print("Detected OS type:", get_os_type() or "NOT SUPPORTED")
diff --git a/gfd/core/routines.py b/gfd/core/routines.py
new file mode 100644
index 0000000..21a9342
--- /dev/null
+++ b/gfd/core/routines.py
@@ -0,0 +1,52 @@
+"""
+routines.py - OS-specific installer routines
+
+Defines a registry of install routines by OS type.
+If the current OS is supported and has an associated installer,
+the matching routine can be executed to perform installation steps.
+"""
+
+from gfd.core.osinfo import get_os_type
+
+
+def install_ubuntu24():
+ """Simulated install routine for Ubuntu 24.04."""
+ print("Running Ubuntu 24.04 (DEB) installation routine...")
+
+
+def install_debian():
+ """Simulated install routine for Debian."""
+ print("Running Debian installation routine (using Ubuntu 24 package)...")
+
+
+SUPPORTED_ROUTINES = {
+ "ubuntu24": install_ubuntu24,
+ "debian": install_debian,
+}
+
+
+def run_install_routine(os_type=None):
+ """
+ Run the appropriate install routine for the given OS type.
+
+ Args:
+ os_type (str | None): OS key (e.g. 'ubuntu24', 'macos').
+ If None, automatically detects current OS.
+
+ Returns:
+ bool: True if the OS is supported and a routine was executed, else False.
+ """
+ os_type = os_type or get_os_type()
+ if not os_type:
+ return False
+
+ routine = SUPPORTED_ROUTINES.get(os_type)
+ if not routine:
+ return False
+
+ routine()
+ return True
+
+
+if __name__ == "__main__":
+ run_install_routine()
diff --git a/gfd/core/sfd.py b/gfd/core/sfd.py
new file mode 100644
index 0000000..5f6aa36
--- /dev/null
+++ b/gfd/core/sfd.py
@@ -0,0 +1,116 @@
+"""
+sfd.py - Installer List Parser for Soporte Firma Digital
+
+This module is responsible for fetching and parsing the list of available
+digital signature installers from the official Soporte Firma Digital website.
+
+It retrieves both the visible installer names (from the page’s