#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SkillFishOS HUD — scegliere cosa mostrare sul desktop, e come.
#
# PERCHE' ESISTE
# Il HUD era una configurazione di conky scritta a mano sulla BC-250: sedici
# barre perche' quella scheda ha sedici thread, i sensori di amdgpu, la ventola
# del nct6686. Su un PC diverso quelle voci tornano vuote, conky non riesce a
# misurare l'ingombro del testo e riduce la finestra a un puntino di 15x15
# pixel. Per questo era stato limitato alla sola BC-250.
# Poi skillfish-hud-config ha imparato a generarla dai sensori che la macchina
# espone davvero, e il HUD ha ricominciato a funzionare ovunque — ma sempre con
# le voci e l'ordine che avevamo scelto noi. Qui sceglie chi la macchina ce
# l'ha davanti.
#
# ⚠️ QUESTA FINESTRA NON SCRIVE LA CONFIGURAZIONE DI CONKY. Scrive solo le
# preferenze in ~/.config/skillfish/hud.json e chiama skillfish-hud-config, che
# resta l'unico posto che sa tradurre una scelta in una riga di conky. Due posti
# che scrivono lo stesso file sono due posti che prima o poi divergono.
#
# ⚠️ E non serve root: il HUD e le sue preferenze stanno nella cartella
# dell'utente. E' l'unica delle nostre applicazioni che non chiede la password,
# ed e' giusto cosi' — non tocca niente di sistema.
import io
import json
import os
import subprocess
import sys
import time

from PyQt6.QtCore import Qt, QTimer, QRect
from PyQt6.QtGui import QIcon, QPainter, QColor, QPen, QBrush, QFont
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QLabel, QPushButton,
                             QCheckBox, QSpinBox, QComboBox, QMessageBox,
                             QListWidget, QListWidgetItem, QToolButton,
                             QSizePolicy)

ICON = "/usr/share/icons/hicolor/256x256/apps/skillfish-hud.png"
VAL = "/usr/local/bin/skillfish-hud-val"
GEN = "/usr/local/bin/skillfish-hud-config"
PREF = os.path.expanduser("~/.config/skillfish/hud.json")
AUTO = os.path.expanduser("~/.config/autostart/skillfish-conky.desktop")
AUTO_SKEL = "/etc/skel/.config/autostart/skillfish-conky.desktop"

# ⚠️ I DATI E LA DISPONIBILITA' NON SI CALCOLANO QUI.
# Stanno in /usr/share/skillfish/hud_dati.py, che usa anche il Remote
# Manager per servire lo stesso configuratore da remoto. Se ognuno dei due
# se li calcolasse per conto suo, al primo sensore nuovo uno direbbe di si'
# e l'altro di no — e l'utente non avrebbe modo di sapere quale ha ragione.
sys.path.insert(0, "/usr/share/skillfish")
import hud_dati


# ---------- i18n ----------
def _detect_lang():
    val = (os.environ.get("LC_ALL") or os.environ.get("LC_MESSAGES")
           or os.environ.get("LANG") or os.environ.get("LANGUAGE") or "")
    v = val.lower()
    if v.startswith("it"):
        return "it"
    if v.startswith("ua"):
        return "uk"
    c = v.replace("-", "_").split(".")[0].split("@")[0].split("_")[0].strip()
    return c if len(c) == 2 and c.isalpha() else "en"


LANG = _detect_lang()

try:
    sys.path.insert(0, "/usr/share/skillfish")
    from i18n import traduttore as _traduttore
    _TR = _traduttore(LANG)
except Exception:
    def _TR(en):
        return en


def L(it, en):
    u"""⚠️ Tutto cio' che chiama L() deve stare sotto questa riga: il corpo del
    modulo si esegue dall'alto in basso, e una costante che chiama L() venti
    righe piu' su ha gia' impedito a una nostra finestra di aprirsi."""
    return it if LANG == "it" else _TR(en)


try:
    from aiuto import Aiuto
except Exception:
    class Aiuto(QToolButton):
        def __init__(self, testo, titolo="", parent=None):
            super().__init__(parent)
            self.setText("?")
            self._t, self._ti = testo, titolo
            self.clicked.connect(self._mostra)

        def _mostra(self):
            QMessageBox.information(self.window(), self._ti or L("Aiuto", "Help"), self._t)


# I blocchi che skillfish-hud-config sa scrivere. La chiave e' la stessa: qui e
# li' si chiamano allo stesso modo, o l'ordine scritto non verrebbe capito.
def blocchi():
    return [
        ("titolo", L("Nome e ora", "Name and clock")),
        ("kernel", L("Versione del kernel", "Kernel version")),
        ("cpu", L("CPU: frequenza, temperatura, carico", "CPU: clock, temperature, load")),
        ("barre", L("Barre dei thread", "Thread bars")),
        ("gpu", L("GPU: frequenza, temperatura, carico", "GPU: clock, temperature, load")),
        ("gpubar", L("Barra del carico GPU", "GPU load bar")),
        ("volt", L("Tensione e watt della GPU", "GPU voltage and watts")),
        ("vram", L("Memoria video", "Video memory")),
        ("ram", L("Memoria", "Memory")),
        ("fan", L("Ventole", "Fans")),
        ("disco", L("Disco", "Disk")),
        ("rete", L("Rete", "Network")),
        ("uptime", L("Uptime", "Uptime")),
        ("bluetooth", L("Bluetooth", "Bluetooth")),
    ]


AIUTO = {
    "voci": (L("Che cosa mostrare", "What to show"), L(
        "Spunta le voci che vuoi e trascinale per cambiarne l'ordine: il HUD le scrive "
        "una sotto l'altra, in quest'ordine.\n\n"
        "⚠️ Una voce spenta dal grigio non e' un divieto: vuol dire che questa macchina "
        "non ha quel dato. La memoria video non esiste su un portatile con grafica "
        "integrata, le ventole non si vedono finche' non si carica il driver del chip "
        "Super-I/O. Sceglierla non fa danni — semplicemente non verra' scritta.\n\n"
        "⚠️ Se le spegni tutte resta il nome: una finestra senza testo conky non riesce "
        "a misurarla e la riduce a un puntino di quindici pixel in un angolo. E' il "
        "guasto per cui il HUD era stato limitato alla sola BC-250.",
        "Tick what you want and drag to reorder: the HUD writes them one under the "
        "other, in this order.\n\n"
        "⚠️ An entry greyed out is not forbidden: it means this machine does not have "
        "that reading. Video memory does not exist on a laptop with integrated "
        "graphics; fans stay invisible until the Super-I/O driver is loaded. Choosing "
        "it does no harm — it simply will not be written.\n\n"
        "⚠️ Turn everything off and the name stays. A window with no text is one conky "
        "cannot measure, and it shrinks to a fifteen-pixel dot in a corner. That is the "
        "fault that had the HUD restricted to the BC-250 in the first place.")),

    "mostra": (L("Mostrare il HUD", "Showing the HUD"), L(
        "Toglie o rimette il pannello dal desktop. Vale subito e vale anche dopo il "
        "riavvio: si scrive nell'avvio automatico, non si limita a chiudere la "
        "finestra.\n\n"
        "Spegnendolo il file dell'avvio automatico resta, solo disattivato: così "
        "riaccenderlo è una spunta e non c'è niente da ricostruire.\n\n"
        "Le scelte su cosa mostrare restano come le hai lasciate: le ritrovi quando lo "
        "riaccendi.",
        "Takes the panel off the desktop, or puts it back. It applies now and it "
        "applies after a reboot: it is written into the autostart entry, not just a "
        "window being closed.\n\n"
        "Turning it off leaves the autostart file in place, only disabled, so turning "
        "it back on is one tick with nothing to rebuild.\n\n"
        "What you chose to show stays as you left it, and is there when you switch it "
        "back on.")),

    "aspetto": (L("Come e dove", "How and where"), L(
        "• POSIZIONE: a quale angolo dello schermo si aggancia.\n"
        "• LARGHEZZA: quanto è largo in pixel. ⚠️ Stringendolo, le righe lunghe vengono "
        "tagliate: la riga della memoria video con tutto per esteso non ci sta sotto i "
        "240 pixel, ed è per questo che di serie è scritta in forma compatta.\n"
        "• TRASPARENZA: 0 è invisibile, 255 è pieno.\n"
        "• CARATTERE: la dimensione di base. Il nome e la versione del kernel si "
        "regolano da soli in proporzione.\n"
        "• AGGIORNAMENTO: ogni quanti secondi rilegge i valori. Sotto il secondo si "
        "spreca corrente per niente: i sensori non cambiano così in fretta.",
        "• POSITION: which corner of the screen it hangs from.\n"
        "• WIDTH: how wide, in pixels. ⚠️ Narrow it and long lines get cut: the video "
        "memory line written out in full does not fit below 240 pixels, which is why it "
        "ships in short form.\n"
        "• TRANSPARENCY: 0 is invisible, 255 is solid.\n"
        "• FONT: the base size. The name and the kernel version scale with it.\n"
        "• REFRESH: how many seconds between readings. Below a second is current burnt "
        "for nothing: sensors do not change that fast.")),

    "anteprima": (L("L'anteprima", "The preview"), L(
        "Mostra le voci che hai scelto, nell'ordine scelto, con i valori veri di questa "
        "macchina letti adesso.\n\n"
        "⚠️ NON E' CONKY. Serve a decidere cosa mettere e in che ordine, non a contare i "
        "pixel: i caratteri e la spaziatura possono venire un filo diversi. Il HUD vero "
        "lo vedi premendo Applica, che riscrive la configurazione e lo riavvia.",
        "It shows the entries you picked, in the order you picked, with this machine's "
        "real values read just now.\n\n"
        "⚠️ IT IS NOT CONKY. It is for deciding what goes in and in what order, not for "
        "counting pixels: fonts and spacing may come out slightly different. You see the "
        "real HUD by pressing Apply, which rewrites the configuration and restarts it.")),
}


def _aiuto(chiave, parent=None):
    titolo, corpo = AIUTO.get(chiave, (L("Aiuto", "Help"), ""))
    return Aiuto(corpo, titolo, parent)


ORDINE_PREDEFINITO = ["titolo", "kernel", "cpu", "barre", "gpu", "gpubar",
                      "volt", "vram", "ram", "fan", "disco", "bluetooth"]

POSIZIONI = [("top_right", L("in alto a destra", "top right")),
             ("top_left", L("in alto a sinistra", "top left")),
             ("bottom_right", L("in basso a destra", "bottom right")),
             ("bottom_left", L("in basso a sinistra", "bottom left")),
             ("middle_right", L("a destra", "right")),
             ("middle_left", L("a sinistra", "left"))]


val = hud_dati.val
c_e_util = hud_dati.c_e_util


def leggi_pref():
    try:
        with io.open(PREF, encoding="utf-8") as f:
            d = json.load(f)
        return d if isinstance(d, dict) else {}
    except (IOError, OSError, ValueError):
        return {}


class Anteprima(QWidget):
    u"""Come verrà il HUD: le voci scelte, con i valori veri di adesso.

    ⚠️ Non e' conky e non finge di esserlo: serve a decidere cosa mettere e in
    che ordine. Contare i pixel qui sarebbe una promessa che non possiamo
    mantenere — conky ha il suo motore di testo — e prometterlo farebbe
    sembrare rotto il HUD vero quando esce di mezzo pixel diverso.
    """

    OTTONE = QColor("#D8A849")
    CHIARO = QColor("#F1E3C6")
    SPENTO = QColor("#B9A07A")

    def __init__(self, parent=None):
        super().__init__(parent)
        self.voci = []
        self.larghezza = 240
        self.opacita = 165
        self.dim = 9
        self.dati = {}
        self.setMinimumSize(360, 380)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

    def righe(self):
        u"""Le righe da disegnare: (testo_a_sinistra, testo_a_destra, tipo)."""
        d = self.dati
        out = []
        for v in self.voci:
            if v == "titolo":
                out.append(("SkillFishOS", time.strftime("%a %d  %H:%M"), "titolo"))
            elif v == "kernel" and d.get("kernel"):
                out.append((d["kernel"], "", "spento"))
            elif v == "cpu":
                p = [d.get("cpu_mhz_g", "—") + " GHz"]
                if d.get("cpu_temp"):
                    p.append(d["cpu_temp"] + "°C")
                p.append(d.get("cpu_use", "—") + "%")
                out.append(("CPU", "   ".join(p), "riga"))
            elif v == "barre":
                out.append(("", "", "barre"))
            elif v == "gpu":
                p = [x for x in (d.get("gpu_freq"),
                                 (d.get("gpu_temp") + "°C") if d.get("gpu_temp") else "",
                                 (d.get("gpu_util", "0") + "%") if d.get("gpu_util_c_e") else "") if x]
                if p:
                    out.append(("GPU", "  ".join(p), "riga"))
            elif v == "gpubar" and d.get("gpu_util_c_e"):
                out.append(("", "", "barra"))
            elif v == "volt":
                p = []
                if d.get("gpu_mv"):
                    p.append(d["gpu_mv"] + " mV")
                if d.get("gpu_power"):
                    p.append("Power " + d["gpu_power"] + " W")
                if p:
                    # ⚠️ Con l'etichetta vuota questa riga partiva dalla colonna
                    # dei valori e sembrava rientrata rispetto a tutte le altre.
                    # La tensione ha un nome: si scrive.
                    out.append(("VDD", "   ".join(p), "riga"))
            elif v == "vram":
                if d.get("vram") and d.get("vram_tot"):
                    out.append(("VRAM", d["vram"] + "/" + d["vram_tot"], "riga"))
                elif d.get("vram_tot"):
                    out.append(("VRAM", d["vram_tot"] + " MiB", "riga"))
            elif v == "ram":
                out.append(("RAM", d.get("ram", "—"), "riga"))
            elif v == "fan" and (d.get("fan_all") or d.get("fan")):
                out.append(("FAN", (d.get("fan_all") or d.get("fan")) + " rpm", "riga"))
            elif v == "disco":
                out.append(("DISK", d.get("disco", "—"), "riga"))
            elif v == "rete" and d.get("rete"):
                out.append(("NET", d["rete"], "riga"))
            elif v == "uptime":
                out.append(("UP", d.get("uptime", "—"), "riga"))
            elif v == "bluetooth" and d.get("bluetooth"):
                out.append(("── Bluetooth ──", "", "sezione"))
        return out

    def paintEvent(self, ev):
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        p.fillRect(self.rect(), QColor("#0d0b06"))
        larg = min(self.larghezza, self.width() - 24)
        x0 = (self.width() - larg) // 2
        righe = self.righe()
        alt = 28 + sum(22 if t in ("riga", "titolo", "sezione") else
                       (14 if t == "barra" else 9 * max(1, (self.dati.get("thread", 8) + 7) // 8))
                       for _, _, t in righe)
        y0 = max(10, (self.height() - alt) // 2)
        # la finestra del HUD, col suo fondo e la sua trasparenza
        p.setBrush(QBrush(QColor(20, 16, 10, self.opacita)))
        p.setPen(QPen(QColor(216, 168, 73, 90), 1))
        p.drawRoundedRect(QRect(x0, y0, larg, alt), 4, 4)

        # ⚠️ La colonna dei valori si misura sull'etichetta piu' larga invece di
        # essere fissa a 52 pixel. Con un carattere piu' grande, o con una
        # etichetta lunga, i valori finivano addosso al nome o partivano da punti
        # diversi riga per riga — ed e' proprio l'allineamento che si viene a
        # guardare in un'anteprima.
        p.setFont(QFont("DejaVu Sans", self.dim))
        fm = p.fontMetrics()
        colonna = max([fm.horizontalAdvance(s) for s, _, tp in righe
                       if tp == "riga" and s] or [40]) + 12

        y = y0 + 20
        for sin, des, tipo in righe:
            if tipo == "titolo":
                p.setFont(QFont("DejaVu Sans", self.dim + 3, QFont.Weight.Bold))
                p.setPen(QPen(self.OTTONE, 1))
                p.drawText(x0 + 12, y, "SkillFish")
                w = p.fontMetrics().horizontalAdvance("SkillFish")
                p.setPen(QPen(self.CHIARO, 1))
                p.drawText(x0 + 12 + w, y, "OS")
                p.setFont(QFont("DejaVu Sans", self.dim))
                p.drawText(QRect(x0, y - 14, larg - 12, 18),
                           int(Qt.AlignmentFlag.AlignRight), des)
                y += 22
            elif tipo == "spento":
                # stessa dimensione delle altre righe, colore spento: si legge
                # e continua a distinguersi dai sensori
                p.setFont(QFont("DejaVu Sans", self.dim))
                p.setPen(QPen(self.SPENTO, 1))
                p.drawText(x0 + 12, y, sin)
                y += 18
            elif tipo == "sezione":
                p.setFont(QFont("DejaVu Sans", self.dim))
                p.setPen(QPen(self.OTTONE, 1))
                p.drawText(x0 + 12, y, sin)
                y += 22
            elif tipo == "barre":
                n = self.dati.get("thread", 8)
                i = 0
                while i < n:
                    x = x0 + 12
                    for _ in range(min(8, n - i)):
                        p.setPen(Qt.PenStyle.NoPen)
                        p.setBrush(QBrush(QColor(216, 168, 73, 120)))
                        p.drawRect(int(x), int(y - 6), 16, 6)
                        x += 19
                        i += 1
                    y += 9
                y += 4
            elif tipo == "barra":
                p.setPen(Qt.PenStyle.NoPen)
                p.setBrush(QBrush(QColor(216, 168, 73, 120)))
                p.drawRect(x0 + 12, int(y - 6), larg - 32, 6)
                y += 14
            else:
                p.setFont(QFont("DejaVu Sans", self.dim))
                p.setPen(QPen(self.SPENTO, 1))
                p.drawText(x0 + 12, y, sin)
                p.setPen(QPen(self.CHIARO, 1))
                p.drawText(x0 + 12 + colonna, y, des)
                y += 22


def hud_acceso():
    u"""Il HUD si accenderà al prossimo accesso?

    Si guarda l'avvio automatico, non se conky sta girando adesso: chi lo
    spegne vuole che resti spento anche dopo un riavvio, e chi lo accende vuole
    ritrovarlo. `Hidden=true` è il modo previsto dallo standard per disattivare
    un avvio automatico senza cancellare il file — cancellarlo vorrebbe dire
    non sapere più come rimetterlo.
    """
    try:
        with io.open(AUTO, encoding="utf-8") as f:
            return "hidden=true" not in f.read().lower()
    except (IOError, OSError):
        return False


def hud_accendi(acceso):
    u"""Scrive l'avvio automatico e fa partire o fermare conky adesso.

    ⚠️ Sono due cose separate: quello che succede al prossimo accesso (il file)
    e quello che succede adesso (il processo). Farne una sola sarebbe sbagliato
    in tutti e due i modi — spegnere senza fermarlo lascia il HUD a schermo fino
    al riavvio, e fermarlo senza scrivere lo fa tornare da solo domani.
    """
    cartella = os.path.dirname(AUTO)
    if not os.path.isdir(cartella):
        try:
            os.makedirs(cartella)
        except OSError:
            pass
    # ⚠️ Se il file non c'è si ricopia da /etc/skel invece di scriverlo qui:
    # quel file ha dentro una lezione (il percorso assoluto senza virgolette,
    # perché KDE converte l'autostart in un servizio systemd e in quel passaggio
    # $HOME resta letterale), e riscriverlo a mano la perderebbe.
    if not os.path.exists(AUTO) and os.path.exists(AUTO_SKEL):
        try:
            with io.open(AUTO_SKEL, encoding="utf-8") as s:
                testo = s.read()
            with io.open(AUTO, "w", encoding="utf-8") as d:
                d.write(testo)
        except (IOError, OSError):
            pass
    try:
        with io.open(AUTO, encoding="utf-8") as f:
            righe = [r for r in f.read().split("\n")
                     if not r.lower().startswith("hidden=")]
        if not acceso:
            righe.append("Hidden=true")
        with io.open(AUTO, "w", encoding="utf-8") as f:
            f.write("\n".join(righe).rstrip() + "\n")
    except (IOError, OSError):
        pass
    subprocess.call(["pkill", "-x", "conky"])
    if acceso:
        try:
            subprocess.Popen(["/usr/local/bin/skillfish-hud"],
                             stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except Exception:
            pass


class Finestra(QMainWindow):

    def __init__(self):
        super().__init__()
        self.setWindowTitle(L("SkillFishOS HUD", "SkillFishOS HUD"))
        if os.path.exists(ICON):
            self.setWindowIcon(QIcon(ICON))
        self.pref = leggi_pref()

        centro = QWidget()
        self.setCentralWidget(centro)
        fuori = QVBoxLayout(centro)
        fuori.setContentsMargins(10, 10, 10, 10)
        fuori.setSpacing(8)

        corpo = QHBoxLayout()
        corpo.setSpacing(10)
        fuori.addLayout(corpo, 1)

        # -- a sinistra: che cosa mostrare, e in che ordine -------------------
        col = QVBoxLayout()
        col.setSpacing(6)
        intest = QHBoxLayout()
        intest.addWidget(QLabel("<b>%s</b>" % L("Che cosa mostrare", "What to show")))
        intest.addWidget(_aiuto("voci", self))
        intest.addStretch(1)
        col.addLayout(intest)
        self.elenco = QListWidget()
        self.elenco.setDragDropMode(QListWidget.DragDropMode.InternalMove)
        self.elenco.setFixedWidth(300)
        self.elenco.itemChanged.connect(lambda _i: self.ridisegna())
        self.elenco.model().rowsMoved.connect(lambda *_a: self.ridisegna())
        col.addWidget(self.elenco, 1)
        corpo.addLayout(col)

        # -- a destra: l'anteprima, che e' la cosa che si guarda --------------
        destra = QVBoxLayout()
        destra.setSpacing(6)
        cima = QHBoxLayout()
        cima.addWidget(QLabel("<b>%s</b>" % L("Anteprima", "Preview")))
        cima.addWidget(_aiuto("anteprima", self))
        cima.addStretch(1)
        self.et_stato = QLabel("")
        self.et_stato.setStyleSheet("color:#9a8a70;")
        cima.addWidget(self.et_stato)
        destra.addLayout(cima)
        self.anteprima = Anteprima()
        destra.addWidget(self.anteprima, 1)
        corpo.addLayout(destra, 1)

        # -- in basso: come e dove --------------------------------------------
        basso = QHBoxLayout()
        basso.setSpacing(10)
        self.mostra = QCheckBox(L("Mostra il HUD", "Show the HUD"))
        self.mostra.setChecked(hud_acceso())
        self.mostra.setToolTip(L(
            "Spento resta spento anche dopo il riavvio.",
            "Off stays off after a reboot too."))
        basso.addWidget(self.mostra)
        basso.addWidget(_aiuto("mostra", self))
        basso.addSpacing(10)
        basso.addWidget(QLabel("<b>%s</b>" % L("Aspetto", "Look")))
        basso.addWidget(_aiuto("aspetto", self))

        self.posizione = QComboBox()
        for chiave, testo in POSIZIONI:
            self.posizione.addItem(testo, chiave)
        i = self.posizione.findData(self.pref.get("posizione", "top_right"))
        self.posizione.setCurrentIndex(max(0, i))
        basso.addWidget(self.posizione)

        def numero(etichetta, chiave, minimo, massimo, ripiego, suffisso=""):
            basso.addWidget(QLabel(etichetta))
            s = QSpinBox()
            s.setRange(minimo, massimo)
            s.setValue(int(self.pref.get(chiave, ripiego)))
            if suffisso:
                s.setSuffix(suffisso)
            s.valueChanged.connect(lambda _v: self.ridisegna())
            basso.addWidget(s)
            return s

        self.larghezza = numero(L("Larghezza", "Width"), "larghezza", 140, 640, 240, " px")
        self.opacita = numero(L("Trasparenza", "Transparency"), "opacita", 0, 255, 165)
        self.dim = numero(L("Carattere", "Font"), "font", 6, 18, 9)
        self.intervallo = numero(L("Aggiornamento", "Refresh"), "intervallo", 1, 30, 2, " s")

        basso.addStretch(1)
        b_def = QPushButton(L("Predefinito", "Default"))
        b_def.clicked.connect(self.predefinito)
        basso.addWidget(b_def)
        self.b_applica = QPushButton(L("Applica", "Apply"))
        self.b_applica.clicked.connect(self.applica)
        basso.addWidget(self.b_applica)
        fuori.addLayout(basso)

        self.riempi_elenco()
        self.aggiorna_dati()
        self._adatta()
        self.orologio = QTimer(self)
        self.orologio.timeout.connect(self.aggiorna_dati)
        self.orologio.start(2000)

    def _adatta(self):
        sc = QApplication.primaryScreen()
        d = sc.availableGeometry() if sc else QRect(0, 0, 1280, 800)
        larg = min(1180, max(860, int(d.width() * 0.62)))
        alt = min(820, max(560, int(d.height() * 0.7)))
        self.resize(larg, alt)
        self.move(d.left() + max(0, (d.width() - larg) // 2),
                  d.top() + max(0, (d.height() - alt) // 2))

    # ---------------------------------------------------------------- elenco
    def riempi_elenco(self):
        u"""Le voci scelte in cima nel loro ordine, poi tutte le altre.

        ⚠️ Le voci che questa macchina non puo' dare restano in elenco, in
        grigio e con scritto perche'. Nasconderle farebbe pensare che
        l'applicazione sia piu' povera di quello che e', e chi collega una
        scheda video domani non capirebbe dove cercare."""
        scelte = self.pref.get("voci")
        if not isinstance(scelte, list):
            scelte = list(ORDINE_PREDEFINITO)
        tutti = dict(blocchi())
        ordine = [k for k in scelte if k in tutti] + \
                 [k for k, _ in blocchi() if k not in scelte]
        self.elenco.blockSignals(True)
        self.elenco.clear()
        for k in ordine:
            it = QListWidgetItem(tutti[k])
            it.setData(Qt.ItemDataRole.UserRole, k)
            it.setFlags(it.flags() | Qt.ItemFlag.ItemIsUserCheckable)
            it.setCheckState(Qt.CheckState.Checked if k in scelte
                             else Qt.CheckState.Unchecked)
            self.elenco.addItem(it)
        self.elenco.blockSignals(False)

    def scelte(self):
        out = []
        for i in range(self.elenco.count()):
            it = self.elenco.item(i)
            if it.checkState() == Qt.CheckState.Checked:
                out.append(it.data(Qt.ItemDataRole.UserRole))
        return out

    def predefinito(self):
        self.pref = {}
        self.riempi_elenco()
        self.posizione.setCurrentIndex(0)
        self.larghezza.setValue(240)
        self.opacita.setValue(165)
        self.dim.setValue(9)
        self.intervallo.setValue(2)
        self.ridisegna()

    # ------------------------------------------------------------------ dati
    def aggiorna_dati(self):
        d = hud_dati.dati()
        self.anteprima.dati = d
        self.segna_disponibili()
        self.ridisegna()

    def segna_disponibili(self):
        u"""Le voci che questa macchina non puo' dare vanno in grigio, col perche'.

        ⚠️ Restano scegliibili. Sono spente perche' il dato non c'e' ADESSO — le
        ventole non si vedono finche' non e' caricato il driver del Super-I/O, e
        una scheda video la si puo' infilare domani. Toglierle dall'elenco
        farebbe sembrare l'applicazione piu' povera di quello che e', e non ci
        sarebbe modo di capire dove cercare quando il dato compare.
        """
        d = self.anteprima.dati
        motivo = L("questa macchina non dà questo dato",
                   "this machine does not provide this reading")
        c_e = hud_dati.disponibili(d)
        for i in range(self.elenco.count()):
            it = self.elenco.item(i)
            k = it.data(Qt.ItemDataRole.UserRole)
            ok = c_e.get(k, True)      # titolo, cpu, barre, ram, disco: sempre
            nome = dict(blocchi()).get(k, k)
            it.setText(nome if ok else nome + "  —  " + motivo)
            it.setForeground(QColor("#efe6d3") if ok else QColor("#7a6c58"))

    def ridisegna(self):
        self.anteprima.voci = self.scelte()
        self.anteprima.larghezza = self.larghezza.value()
        self.anteprima.opacita = self.opacita.value()
        self.anteprima.dim = self.dim.value()
        self.anteprima.update()

    # -------------------------------------------------------------- applica
    def applica(self):
        u"""Scrive le preferenze, rigenera la configurazione, riavvia il HUD.

        ⚠️ La configurazione la scrive skillfish-hud-config, non questa
        finestra: e' l'unico posto che sa quali dati la macchina ha davvero e
        come si traducono in righe di conky. Qui si scrive solo la scelta."""
        pref = {
            "versione": 1,
            "mostra": self.mostra.isChecked(),
            "voci": self.scelte(),
            "posizione": self.posizione.currentData(),
            "larghezza": self.larghezza.value(),
            "opacita": self.opacita.value(),
            "font": self.dim.value(),
            "intervallo": self.intervallo.value(),
        }
        try:
            cartella = os.path.dirname(PREF)
            if not os.path.isdir(cartella):
                os.makedirs(cartella)
            tmp = PREF + ".tmp"
            with io.open(tmp, "w", encoding="utf-8") as f:
                f.write(json.dumps(pref, indent=2, ensure_ascii=False, sort_keys=True))
            os.rename(tmp, PREF)
        except (IOError, OSError) as e:
            QMessageBox.critical(self, L("Non riesco a salvare", "Could not save"), str(e))
            return
        self.pref = pref

        try:
            p = subprocess.run([GEN], capture_output=True, text=True, timeout=60)
        except Exception as e:
            QMessageBox.critical(self, L("Errore", "Error"), str(e))
            return
        if p.returncode != 0:
            QMessageBox.critical(self, L("Errore", "Error"),
                                 (p.stderr or p.stdout or "")[:400])
            return
        # ⚠️ conky non rilegge la configurazione da solo: va riavviato, o si
        # applica tutto e sullo schermo non cambia niente — e sembra rotto.
        # hud_accendi fa tutte e due le cose, spegnimento compreso.
        hud_accendi(self.mostra.isChecked())
        self.et_stato.setText(L("applicato", "applied") if self.mostra.isChecked()
                              else L("HUD spento", "HUD off"))


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("SkillFishOS HUD")
    # ⚠️ Senza questa riga la barra delle applicazioni scrive «python3»: KDE lega
    # la finestra al file .desktop attraverso questo nome.
    app.setDesktopFileName("os.skillfish.hud")
    if os.path.exists(ICON):
        app.setWindowIcon(QIcon(ICON))
    f = Finestra()
    f.show()
    return app.exec()


if __name__ == "__main__":
    sys.exit(main())
