feat(windows): ein echtes Setup — Voraussetzungs-Pruefung, Zielordner, Ablage

Commander: "Dann haette ich beim 'Setup' auch erwartet das nen echtes setup
passiert - wo will ich das hinspeichern, nen pre requirement check usw. Da
kommt garnichts Rippy geht einfach auf."

Er hatte recht. Ein Doppelklick installierte stillschweigend nach
%LOCALAPPDATA%\Rippy und oeffnete das Fenster. Der Nutzer wurde nichts
gefragt und erfuhr nichts -- auch nicht, ob sein Rechner ueberhaupt kann, was
Rippy braucht.

Jetzt: ein Assistent mit acht Pruefungen, zwei Ordner-Wahlen und drei
Schaltern. Auf diesem Rechner nachgemessen:

    OK  Betriebssystem      Windows 11 (Build 26200)
    OK  Fenster (WebView2)  151.0.4129.107
    OK  Optisches Laufwerk  \.\G:
    OK  HandBrakeCLI        1.11.2
     !  MakeMKV             nicht gefunden
                            -> wird von makemkv.com geholt
    OK  Speicherplatz       59.1 GB frei
    OK  Schreibrecht        C:\Users\...\AppData\Local\Rippy
    OK  Netzwerk-Port       7788 ist frei

Drei Entwurfsentscheidungen, die dazugehoeren:

* Nur ein FEHLER blockiert, eine Warnung nicht. Eine Warnung, die den Knopf
  sperrt, ist eine Bevormundung; ein Fehler, der nur warnt, ist eine Falle.
  Kein Laufwerk = Warnung (reine Komprimier-Maschine ist ein vorgesehener
  Betriebsfall). Kein Schreibrecht = Fehler.
* Jeder Befund sagt, was zu tun ist. Ein Befund ohne Abhilfe laesst den
  Nutzer ratlos zurueck -- ein Test haelt das fuer jede Pruefung fest.
* Die Pruefungen stehen in einrichtung.py, komplett ohne Fenster pruefbar.
  Im Fenstermodul steht nur Aufbau und Bruecke.

WebView2 statt Win32-Dialog: Es ist ohnehin da (Entscheid 4), der Assistent
kostet damit NULL zusaetzliche Bytes. Die Seite laedt nichts aus dem Netz --
sie muss auf einem Rechner ohne Internet aufgehen, dort wird sie am
dringendsten gebraucht. Ein Test haelt das fest.

--still und --ohne-assistent gehen weiterhin den stummen Weg. Geht das
Fenster nicht auf, wird mit den Vorgaben installiert und gesagt warum -- ein
Setup, das gar nichts tut, weil sein Fenster nicht aufging, waere schlimmer
als eines, das nicht fragt.

fix(tools): %ProgramFiles(x86)% wurde auf echten Rechnern NIE aufgeloest

Beim Bauen der Pruefung aufgefallen, und es ist ein Lehrstueck:

    Testumgebung   {"ProgramFiles(x86)": ...}  ->  C:\Program Files (x86)\...
    echtes Windows {"PROGRAMFILES(X86)": ...}  ->  ''

Windows behandelt Umgebungsvariablen ohne Ruecksicht auf die Schreibweise und
legt sie in os.environ GROSS ab. Der Vergleich in _entfalten war exakt, fand
nie eine Uebereinstimmung, und die bekannten Installationsorte fielen aus der
Kandidatenliste. MakeMKV wurde also NUR ueber die Registry-Rueckfallebene
gefunden -- wo die fehlt oder anders aussieht, fand Rippy nichts.

Aufgefallen ist es nur, weil hier zum ersten Mal eine ECHTE Umgebung benutzt
wurde. Der Test spritzte genau die Schreibweise ein, die im Muster stand: Er
war gruener als die Wirklichkeit. Beide Tests nehmen jetzt die echte
Schreibweise, und einer prueft direkt gegen os.environ. Beim Zurueckdrehen
des Fehlers rot gesehen (5 Fehlschlaege).

Ampel lokal: 691 gruen, ruff sauber. Assistent im Bildschirmfoto belegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-28 13:54:59 +02:00
co-authored by Claude Opus 5
parent 66642b8d93
commit a4a045f303
7 changed files with 1073 additions and 6 deletions
+260
View File
@@ -0,0 +1,260 @@
"""Der Einrichtungs-Assistent: prüfen, fragen, installieren.
## Der Befund des Commanders (28.08.2026)
> „Dann hätte ich beim Setup' auch erwartet das nen echtes setup passiert —
> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt
> garnichts Rippy geht einfach auf."
Er hat recht. Ein Doppelklick auf `RippySetup.exe` installierte stillschweigend
nach `%LOCALAPPDATA%\\Rippy` und öffnete das Fenster. Der Nutzer wurde nichts
gefragt und erfuhr nichts — auch nicht, ob sein Rechner überhaupt kann, was
Rippy braucht. Wenn hinterher etwas fehlte, stand er davor und wusste nicht,
seit wann.
## Was hier passiert und was NICHT
Dieses Modul enthält die **Prüfungen und die Entscheidungen** — keine
Oberfläche. Das Fenster steckt in `setup_fenster.py`, damit hier alles ohne
WebView2, ohne Fenster und ohne Installation prüfbar bleibt.
## Die drei Sorten Befund
ok geht
warnung geht, aber der Nutzer sollte es wissen
fehler geht NICHT — die Installation würde nichts nützen
Nur `fehler` blockiert. Eine Warnung, die den Knopf sperrt, ist eine
Bevormundung; ein Fehler, der nur warnt, ist eine Falle. Ein Beispiel für
jede Sorte:
* **Kein optisches Laufwerk** ist eine WARNUNG. Rippy ohne Laufwerk ist eine
reine Komprimier-Maschine — ein vorgesehener Betriebsfall (`install.sh` sagt
das seit Langem: „läuft trotzdem durch").
* **Kein MakeMKV** ist eine WARNUNG, solange Rippy es holen kann.
* **Kein Schreibrecht am Zielordner** ist ein FEHLER. Da hilft kein Zureden.
## Warum jede Prüfung sagt, was zu tun ist
Ein Befund ohne Abhilfe lässt den Nutzer ratlos zurück. Deshalb hat jeder
Eintrag ein `abhilfe`-Feld — leer nur dann, wenn wirklich nichts zu tun ist.
"""
import os
import shutil
import socket
import sys
OK = "ok"
WARNUNG = "warnung"
FEHLER = "fehler"
# Was Rippy mindestens braucht. Gemessen, nicht geschätzt: Das Paket allein ist
# 56 MB, dazu die entpackte HandBrake-Fassung (~74 MB), die Datenbank und der
# WebView2-Zwischenspeicher. 500 MB sind reichlich und lassen Luft.
MINDESTPLATZ_MB = 500
STANDARD_PORT = 7788
def _eintrag(name, zustand, text, abhilfe=""):
return {"name": name, "zustand": zustand, "text": text, "abhilfe": abhilfe}
# ── Die einzelnen Prüfungen ─────────────────────────────────────────────
def pruefe_windows(version=None) -> dict:
"""Windows 10 oder neuer. Darunter gibt es kein WebView2."""
if os.name != "nt":
return _eintrag("Betriebssystem", FEHLER, "Kein Windows",
"Für Linux gibt es die Docker- und die Headless-Fassung.")
bau = version if version is not None else sys.getwindowsversion().build
if bau >= 22000:
return _eintrag("Betriebssystem", OK, "Windows 11 (Build %d)" % bau)
if bau >= 10240:
return _eintrag("Betriebssystem", OK, "Windows 10 (Build %d)" % bau)
return _eintrag("Betriebssystem", FEHLER, "Windows-Build %d ist zu alt" % bau,
"Rippy braucht mindestens Windows 10.")
def pruefe_webview(version=None) -> dict:
"""Ohne WebView2 gibt es kein Fenster — aber Rippy läuft trotzdem.
Deshalb WARNUNG und nicht Fehler: Der Rückfall auf den Browser
funktioniert, er ist nur weniger schön.
"""
if version is None:
from rippy import fenster
version = fenster.webview2_version()
if version:
return _eintrag("Fenster (WebView2)", OK, version)
return _eintrag(
"Fenster (WebView2)", WARNUNG, "nicht gefunden",
'Rippy öffnet sich dann im Browser. Die Laufzeit gibt es kostenlos '
'bei Microsoft (Suchwort „WebView2 Evergreen Runtime").')
def pruefe_platz(ziel: str, frei_mb=None) -> dict:
"""Genug Platz am Zielort? Gemessen am nächsten vorhandenen Ordner."""
from rippy import betrieb
if frei_mb is None:
pfad = betrieb.naechster_vorhandener(ziel)
if not pfad:
return _eintrag("Speicherplatz", FEHLER,
"Der Ort ist nicht erreichbar: %s" % ziel,
"Einen anderen Zielordner wählen.")
try:
frei_mb = shutil.disk_usage(pfad).free // (1024 * 1024)
except OSError as e:
return _eintrag("Speicherplatz", FEHLER, "Nicht lesbar (%s)" % e,
"Einen anderen Zielordner wählen.")
if frei_mb >= MINDESTPLATZ_MB:
return _eintrag("Speicherplatz", OK, "%.1f GB frei" % (frei_mb / 1024))
return _eintrag("Speicherplatz", FEHLER,
"nur %d MB frei, gebraucht werden %d MB"
% (frei_mb, MINDESTPLATZ_MB),
"Platz schaffen oder einen anderen Zielordner wählen.")
def pruefe_schreibrecht(ziel: str, probe=None) -> dict:
"""Darf Rippy dorthin schreiben? Wirklich probiert, nicht geraten.
Ein `os.access(..., W_OK)` lügt unter Windows: Es beantwortet die Frage
nach dem Schreibrecht anhand des Nur-Lesen-Flags, nicht anhand der
Zugriffsrechte. Also wird eine Datei angelegt und wieder gelöscht.
"""
if probe is not None:
geklappt, grund = probe
else:
geklappt, grund = _schreibprobe(ziel)
if geklappt:
return _eintrag("Schreibrecht", OK, ziel)
return _eintrag("Schreibrecht", FEHLER, "%s (%s)" % (ziel, grund),
'Einen Ordner unterhalb von „Benutzer" wählen — dort '
'braucht Rippy keine Administratorrechte.')
def _schreibprobe(ziel: str):
import tempfile
try:
os.makedirs(ziel, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=ziel, prefix=".rippy-probe",
delete=True):
pass
return True, ""
except OSError as e:
return False, e.strerror or str(e)
def pruefe_port(port: int = STANDARD_PORT, belegt=None) -> dict:
"""Ist der Port frei? Wenn nicht, kann Rippy nicht bedienen."""
if belegt is None:
belegt = _port_belegt(port)
if not belegt:
return _eintrag("Netzwerk-Port", OK, "%d ist frei" % port)
# Kein Fehler: Sehr oft ist es ein bereits laufendes Rippy, und dann ist
# eine Neuinstallation genau richtig.
return _eintrag("Netzwerk-Port", WARNUNG, "%d ist belegt" % port,
"Läuft Rippy schon? Sonst im Setup einen anderen Port "
"wählen.")
def _port_belegt(port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
try:
s.bind(("127.0.0.1", port))
return False
except OSError:
return True
def pruefe_laufwerk(laufwerke=None) -> dict:
"""Ein optisches Laufwerk. WARNUNG, kein Fehler.
Rippy ohne Laufwerk ist eine reine Komprimier-Maschine — ein vorgesehener
Betriebsfall, den `install.sh` seit Langem ausdrücklich zulässt.
"""
if laufwerke is None:
try:
from rippy import drives
laufwerke = drives.treiber().list_optical_devices()
except Exception: # noqa: BLE001
laufwerke = []
if laufwerke:
return _eintrag("Optisches Laufwerk", OK,
", ".join(str(x) for x in laufwerke))
return _eintrag("Optisches Laufwerk", WARNUNG, "keins gefunden",
"Ohne Laufwerk kann Rippy nur komprimieren, nicht rippen. "
"Ein USB-Brenner reicht.")
def pruefe_werkzeuge(bestand=None) -> list:
"""MakeMKV und HandBrake — je ein eigener Eintrag.
MakeMKV fehlt => WARNUNG, weil das Setup es holen kann. Ein Fehler wäre
es erst, wenn es danach immer noch fehlt (siehe `tools/einrichten.py`).
"""
from rippy.tools import katalog
bestand = katalog.bestand() if bestand is None else bestand
ergebnis = []
for name, eintrag in sorted(bestand.items()):
titel = eintrag.get("titel", name)
if eintrag.get("da"):
ergebnis.append(_eintrag(titel, OK,
eintrag.get("version") or "gefunden"))
elif name == "handbrake":
ergebnis.append(_eintrag(titel, OK, "wird mitgeliefert"))
else:
ergebnis.append(_eintrag(
titel, WARNUNG, "nicht gefunden",
"Das Setup lädt es von makemkv.com und startet den Installer "
"des Herstellers. Ohne MakeMKV kann Rippy keine Disc lesen."))
return ergebnis
# ── Alles zusammen ──────────────────────────────────────────────────────
def alle_pruefungen(ziel: str, port: int = STANDARD_PORT) -> list:
"""Der vollständige Voraussetzungs-Bericht, in Anzeigereihenfolge."""
return ([pruefe_windows(), pruefe_webview(), pruefe_laufwerk()]
+ pruefe_werkzeuge()
+ [pruefe_platz(ziel), pruefe_schreibrecht(ziel), pruefe_port(port)])
def darf_installieren(pruefungen: list) -> bool:
"""Blockiert NUR bei einem echten Fehler. (reine Funktion)
Eine Warnung, die den Knopf sperrt, ist eine Bevormundung; ein Fehler,
der nur warnt, ist eine Falle.
"""
return not any(p["zustand"] == FEHLER for p in pruefungen)
def zusammenfassung(pruefungen: list) -> str:
"""Ein Satz über den Gesamtzustand. (reine Funktion)"""
fehler = [p for p in pruefungen if p["zustand"] == FEHLER]
warnungen = [p for p in pruefungen if p["zustand"] == WARNUNG]
if fehler:
return "Installation nicht möglich: " + ", ".join(p["name"] for p in fehler)
if warnungen:
return "Bereit — mit Hinweisen zu: " + ", ".join(p["name"] for p in warnungen)
return "Alles bereit."
def vorgaben() -> dict:
"""Womit die Maske vorbelegt wird."""
from rippy import betrieb
from rippy.platform import win_registry as reg
return {
"ziel": reg.standard_installationsordner(),
"ablage": betrieb.ablage_vorgabe({}, container=False, system="windows"),
"port": STANDARD_PORT,
"autostart": True,
"verknuepfungen": True,
"werkzeuge_holen": True,
}
+389
View File
@@ -0,0 +1,389 @@
"""Das Fenster des Einrichtungs-Assistenten.
## Warum es das gibt
Commander am 28.08.2026:
> „Dann hätte ich beim Setup' auch erwartet das nen echtes setup passiert —
> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt
> garnichts Rippy geht einfach auf."
## Warum WebView2 und nicht ein Windows-Dialog
Weil es schon da ist. Rippy bringt WebView2 ohnehin für sein Hauptfenster mit
(Entscheid 4 in `KONZEPT-V2.md` § 10) — der Assistent kostet damit **null
zusätzliche Bytes**. Ein Dialog aus Win32-Bausteinen wäre hunderte Zeilen
ctypes für etwas, das hier fünfzig Zeilen HTML sind; `tkinter` ist im Bau
ausdrücklich ausgeschlossen.
## Warum die Prüfungen NICHT hier stehen
Sie stehen in `einrichtung.py`, und zwar vollständig ohne Fenster prüfbar.
Was hier steht, lässt sich nicht testen — also steht hier so wenig wie
möglich: Aufbau, Anzeige, und die Brücke zwischen beidem.
## Was passiert, wenn das Fenster nicht aufgeht
Dann wird ohne Assistent installiert, mit den Vorgaben, und gesagt warum. Ein
Setup, das gar nichts tut, weil sein Fenster nicht aufging, wäre schlimmer
als eines, das nicht fragt.
"""
import threading
from rippy import einrichtung
TITEL = "Rippy einrichten"
BREITE = 860
HOEHE = 760
class Bruecke:
"""Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML.
Jede Methode gibt Daten zurück, keine Erfolgsmeldungen — dieselbe Regel
wie überall sonst in diesem Projekt.
"""
def __init__(self):
self.ergebnis = None
self._fenster = None
self._fortschritt = []
# ── Auskunft ────────────────────────────────────────────────────────
def vorgaben(self) -> dict:
return einrichtung.vorgaben()
def pruefen(self, ziel: str = "", port: int = 0) -> dict:
"""Der Voraussetzungs-Bericht für die angegebenen Werte."""
vorgabe = einrichtung.vorgaben()
befunde = einrichtung.alle_pruefungen(ziel or vorgabe["ziel"],
int(port or vorgabe["port"]))
return {
"befunde": befunde,
"darf": einrichtung.darf_installieren(befunde),
"text": einrichtung.zusammenfassung(befunde),
}
def ordner_waehlen(self, vorschlag: str = "") -> str:
"""Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde."""
import webview
try:
gewaehlt = self._fenster.create_file_dialog(
webview.FOLDER_DIALOG, directory=vorschlag or "")
except Exception: # noqa: BLE001
return ""
if not gewaehlt:
return ""
return gewaehlt[0] if isinstance(gewaehlt, (list, tuple)) else str(gewaehlt)
def fortschritt(self) -> list:
"""Was seit dem letzten Abruf passiert ist. Leert sich dabei."""
zeilen, self._fortschritt = self._fortschritt, []
return zeilen
# ── Handeln ─────────────────────────────────────────────────────────
def installieren(self, optionen: dict) -> dict:
"""Installiert im Hintergrund-Thread und gibt sofort zurück.
Sofort, weil das Holen von MakeMKV Minuten dauern kann: Ein blockierter
Aufruf ließe das Fenster einfrieren, und ein eingefrorenes Fenster
sieht aus wie ein Absturz.
"""
threading.Thread(target=self._installieren_wirklich,
args=(optionen or {},), daemon=True).start()
return {"gestartet": True}
def _installieren_wirklich(self, optionen: dict) -> None:
from rippy import windows_app
def melden(text, anteil=None):
self._fortschritt.append(str(text))
try:
melden("Dateien werden abgelegt …")
ziel = windows_app.installieren(
ziel=optionen.get("ziel") or None,
autostart=bool(optionen.get("autostart", True)),
starten_danach=False,
verknuepfen=bool(optionen.get("verknuepfungen", True)),
)
if optionen.get("ablage"):
melden("Ablage wird gespeichert …")
_ablage_speichern(optionen["ablage"])
if optionen.get("werkzeuge_holen", True):
melden("Werkzeuge werden geprüft …")
from rippy.tools import einrichten as werkzeug_einrichtung
bericht = werkzeug_einrichtung.sicherstellen(fortschritt=melden)
melden(werkzeug_einrichtung.bericht(bericht))
bereit = bericht["bereit"]
else:
bereit = True
self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit}
melden("Fertig.")
except Exception as ex: # noqa: BLE001
# Nicht verschlucken: Ohne Konsole waere ein Absturz hier voellig
# unsichtbar, und der Nutzer saehe ein Fenster, in dem nichts mehr
# passiert.
self.ergebnis = {"ok": False,
"fehler": "%s: %s" % (type(ex).__name__, ex)}
melden("Fehlgeschlagen: %s" % ex)
def stand(self) -> dict:
"""None, solange es läuft. Danach das Ergebnis."""
return self.ergebnis or {}
def schliessen(self) -> None:
try:
self._fenster.destroy()
except Exception: # noqa: BLE001
pass
def _ablage_speichern(pfad: str) -> None:
"""Die gewählte Ablage in die Einstellungen schreiben.
Über die laufende Datenbank, nicht über eine Konfigurationsdatei: Das UI
liest `outputDir` aus den Einstellungen, und genau dort muss der Wert
landen, damit die Wahl im Assistenten überhaupt etwas bewirkt.
"""
from rippy import config, daemon, store
werte = config.laden()
store.verbinden(config.datenbank_url(werte))
import sys
sys.path.insert(0, daemon._api_pfad())
import db as api_db # noqa: E402
api_db.init_db()
aktuell = api_db.get_settings() or {}
aktuell["outputDir"] = pfad
api_db.save_settings(aktuell)
def starten() -> dict:
"""Öffnet den Assistenten. Gibt zurück, was daraus geworden ist.
`{}` heißt: abgebrochen oder nicht möglich. Der Aufrufer entscheidet dann,
ob er ohne Assistent installiert.
"""
import webview
bruecke = Bruecke()
fenster = webview.create_window(TITEL, html=SEITE, js_api=bruecke,
width=BREITE, height=HOEHE,
min_size=(720, 560), text_select=True)
bruecke._fenster = fenster
webview.start(gui="edgechromium", private_mode=False)
return bruecke.ergebnis or {}
def moeglich() -> tuple:
"""Kann der Assistent überhaupt aufgehen? (ja/nein, Begründung)"""
from rippy import fenster as fenster_modul
return fenster_modul.verfuegbar()
# ── Die Seite ───────────────────────────────────────────────────────────
# Bewusst EINE Datei ohne Abhängigkeiten: kein Vite-Bau, kein Nachladen, keine
# Schriftart aus dem Netz. Der Assistent muss auch auf einem Rechner ohne
# Internet aufgehen — dort wird er am dringendsten gebraucht.
SEITE = r"""<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8">
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin:0; background:#0b0f19; color:#e2e8f0;
font:14px/1.55 "Segoe UI",system-ui,sans-serif; }
header { padding:22px 28px 16px; border-bottom:1px solid #1e293b; }
h1 { margin:0; font-size:19px; font-weight:650; }
h1 span { color:#f59e0b; }
.unter { color:#94a3b8; font-size:12.5px; margin-top:4px; }
main { padding:20px 28px 96px; }
section { margin-bottom:22px; }
h2 { font-size:12px; text-transform:uppercase; letter-spacing:.09em;
color:#94a3b8; margin:0 0 10px; font-weight:600; }
.zeile { display:flex; gap:10px; padding:7px 0;
border-bottom:1px solid #131c2e; align-items:flex-start; }
.zeile:last-child { border-bottom:0; }
.marke { width:17px; flex:0 0 17px; font-weight:700; text-align:center; }
.ok .marke { color:#34d399; } .warnung .marke { color:#fbbf24; }
.fehler .marke { color:#f87171; }
.inhalt { flex:1; min-width:0; }
.name { font-weight:600; }
.wert { color:#cbd5e1; font-family:ui-monospace,Consolas,monospace;
font-size:12.5px; word-break:break-all; }
.abhilfe { color:#94a3b8; font-size:12px; margin-top:2px; }
label.feld { display:block; margin-bottom:14px; }
label.feld > span { display:block; font-size:12.5px; color:#cbd5e1;
margin-bottom:5px; }
.mitknopf { display:flex; gap:8px; }
input[type=text], input[type=number] {
flex:1; background:#0f1626; border:1px solid #24324a; border-radius:7px;
color:#e2e8f0; padding:9px 11px; font-size:13px; font-family:inherit; }
input:focus { outline:2px solid #f59e0b55; border-color:#f59e0b; }
button { background:#1c2740; border:1px solid #2e3d5c; color:#e2e8f0;
border-radius:7px; padding:9px 15px; font-size:13px; cursor:pointer;
font-family:inherit; }
button:hover:not(:disabled) { background:#25324f; }
button.los { background:#f59e0b; border-color:#f59e0b; color:#1a1206;
font-weight:650; padding:11px 26px; }
button.los:hover:not(:disabled) { background:#fbbf24; }
button:disabled { opacity:.45; cursor:not-allowed; }
.schalter { display:flex; gap:9px; align-items:flex-start; margin-bottom:11px;
cursor:pointer; }
.schalter input { margin:3px 0 0; accent-color:#f59e0b; }
.schalter .hinweis { color:#94a3b8; font-size:12px; }
footer { position:fixed; left:0; right:0; bottom:0; background:#0b0f19f2;
border-top:1px solid #1e293b; padding:14px 28px;
display:flex; align-items:center; gap:16px;
backdrop-filter:blur(6px); }
#urteil { flex:1; font-size:12.5px; color:#94a3b8; }
#protokoll { background:#080c15; border:1px solid #1a2438; border-radius:8px;
padding:11px 13px; font-family:ui-monospace,Consolas,monospace;
font-size:12px; color:#94a3b8; max-height:190px; overflow:auto;
white-space:pre-wrap; }
.weg { display:none; }
</style></head><body>
<header>
<h1><span>&#9673;</span> Rippy einrichten</h1>
<div class="unter">Wird geprüft, was dein Rechner mitbringt — danach entscheidest du, wohin.</div>
</header>
<main>
<section id="teil-pruefung">
<h2>Voraussetzungen</h2>
<div id="pruefungen"><div class="zeile"><div class="inhalt wert">wird geprüft …</div></div></div>
</section>
<section id="teil-orte">
<h2>Wohin</h2>
<label class="feld"><span>Rippy selbst (Programmordner)</span>
<div class="mitknopf">
<input type="text" id="ziel">
<button onclick="waehlen('ziel')">Durchsuchen …</button>
</div>
</label>
<label class="feld"><span>Fertige Filme (Ablage) — darf auch ein Netzwerkpfad sein</span>
<div class="mitknopf">
<input type="text" id="ablage">
<button onclick="waehlen('ablage')">Durchsuchen …</button>
</div>
</label>
<label class="feld" style="max-width:190px"><span>Port</span>
<input type="number" id="port" min="1024" max="65535">
</label>
</section>
<section id="teil-optionen">
<h2>Sonst noch</h2>
<label class="schalter"><input type="checkbox" id="autostart" checked>
<span>Mit Windows starten<div class="hinweis">Rippy läuft dann im Hintergrund und erkennt eingelegte Discs von allein.</div></span></label>
<label class="schalter"><input type="checkbox" id="verknuepfungen" checked>
<span>Symbol auf dem Desktop und im Startmenü<div class="hinweis">Ohne das ist Rippy praktisch unauffindbar.</div></span></label>
<label class="schalter"><input type="checkbox" id="werkzeuge_holen" checked>
<span>Fehlende Werkzeuge holen<div class="hinweis">HandBrake liegt bei. MakeMKV wird von makemkv.com geladen und mit dem Installer des Herstellers installiert — ohne MakeMKV kann Rippy keine Disc lesen.</div></span></label>
</section>
<section id="teil-lauf" class="weg">
<h2>Läuft</h2>
<div id="protokoll"></div>
</section>
</main>
<footer>
<div id="urteil">wird geprüft …</div>
<button id="knopf-abbruch" onclick="pywebview.api.schliessen()">Abbrechen</button>
<button id="knopf-los" class="los" onclick="losgehen()" disabled>Installieren</button>
</footer>
<script>
const $ = (id) => document.getElementById(id);
let laeuft = false;
function zeichnePruefungen(bericht) {
const marken = { ok: '&#10003;', warnung: '!', fehler: '&#10007;' };
$('pruefungen').innerHTML = bericht.befunde.map(b =>
'<div class="zeile ' + b.zustand + '">'
+ '<div class="marke">' + (marken[b.zustand] || '?') + '</div>'
+ '<div class="inhalt"><div class="name">' + b.name + '</div>'
+ '<div class="wert">' + b.text + '</div>'
+ (b.abhilfe ? '<div class="abhilfe">' + b.abhilfe + '</div>' : '')
+ '</div></div>').join('');
$('urteil').textContent = bericht.text;
$('knopf-los').disabled = !bericht.darf || laeuft;
}
async function pruefen() {
if (laeuft) return;
zeichnePruefungen(await pywebview.api.pruefen($('ziel').value, Number($('port').value)));
}
async function waehlen(feld) {
const ordner = await pywebview.api.ordner_waehlen($(feld).value);
if (ordner) { $(feld).value = ordner; if (feld === 'ziel') pruefen(); }
}
async function losgehen() {
laeuft = true;
$('knopf-los').disabled = true;
$('knopf-los').textContent = 'Läuft …';
$('teil-lauf').classList.remove('weg');
$('urteil').textContent = 'Rippy wird eingerichtet …';
await pywebview.api.installieren({
ziel: $('ziel').value,
ablage: $('ablage').value,
port: Number($('port').value),
autostart: $('autostart').checked,
verknuepfungen: $('verknuepfungen').checked,
werkzeuge_holen: $('werkzeuge_holen').checked,
});
verfolgen();
}
async function verfolgen() {
const neue = await pywebview.api.fortschritt();
if (neue.length) {
const p = $('protokoll');
p.textContent += neue.join('\n') + '\n';
p.scrollTop = p.scrollHeight;
}
const stand = await pywebview.api.stand();
if (stand && stand.ok !== undefined) {
$('knopf-abbruch').classList.add('weg');
$('knopf-los').disabled = false;
$('knopf-los').textContent = 'Rippy öffnen';
$('knopf-los').onclick = () => pywebview.api.schliessen();
$('urteil').textContent = stand.ok
? (stand.bereit ? 'Fertig — Rippy ist einsatzbereit.'
: 'Installiert, aber es fehlt noch ein Werkzeug (siehe oben).')
: ('Fehlgeschlagen: ' + stand.fehler);
return;
}
setTimeout(verfolgen, 700);
}
window.addEventListener('pywebviewready', async () => {
const v = await pywebview.api.vorgaben();
$('ziel').value = v.ziel;
$('ablage').value = v.ablage;
$('port').value = v.port;
$('autostart').checked = v.autostart;
$('verknuepfungen').checked = v.verknuepfungen;
$('werkzeuge_holen').checked = v.werkzeuge_holen;
pruefen();
$('port').addEventListener('change', pruefen);
$('ziel').addEventListener('change', pruefen);
});
</script>
</body></html>
"""
# Damit die Seite auch ohne laufendes Fenster prüfbar ist (siehe Tests).
SEITE_LAENGE = len(SEITE)
def seite_enthaelt(text: str) -> bool:
"""Kleine Hilfe für Tests: Steht etwas in der Seite?"""
return text in SEITE
+188
View File
@@ -0,0 +1,188 @@
"""Der Einrichtungs-Assistent — die Pruefungen, ohne etwas zu installieren.
## Warum es diese Tests gibt
Commander am 28.08.2026: *„Dann haette ich beim Setup' auch erwartet das nen
echtes setup passiert — wo will ich das hinspeichern, nen pre requirement
check usw. Da kommt garnichts Rippy geht einfach auf."*
Er hatte recht: Ein Doppelklick installierte stillschweigend und oeffnete das
Fenster. Der Nutzer wurde nichts gefragt und erfuhr nichts.
Alle Pruefungen sind eingespritzt — kein Fenster, keine Installation, kein
WebView2. Sonst waere „kein Laufwerk" nur auf einem Rechner ohne Laufwerk
pruefbar, und genau dort sieht es niemand.
"""
import os
from rippy import einrichtung as e
def _p(zustand, name="X"):
return {"name": name, "zustand": zustand, "text": "", "abhilfe": ""}
# ── Was blockiert und was nicht ─────────────────────────────────────────
def test_ohne_fehler_darf_installiert_werden():
assert e.darf_installieren([_p(e.OK), _p(e.WARNUNG)]) is True
def test_ein_fehler_blockiert():
assert e.darf_installieren([_p(e.OK), _p(e.FEHLER)]) is False
def test_eine_warnung_blockiert_NICHT():
"""Eine Warnung, die den Knopf sperrt, ist eine Bevormundung. Rippy ohne
optisches Laufwerk ist ein vorgesehener Betriebsfall — eine reine
Komprimier-Maschine."""
assert e.darf_installieren([_p(e.WARNUNG), _p(e.WARNUNG)]) is True
# ── Die einzelnen Pruefungen ────────────────────────────────────────────
def test_windows_10_und_11_gehen():
assert e.pruefe_windows(22631)["zustand"] == e.OK
assert e.pruefe_windows(19045)["zustand"] == e.OK
def test_zu_altes_windows_ist_ein_fehler():
befund = e.pruefe_windows(7601) # Windows 7
assert befund["zustand"] == e.FEHLER
assert befund["abhilfe"], "ein Befund ohne Abhilfe laesst den Nutzer ratlos"
def test_fehlendes_webview_ist_nur_eine_warnung():
"""Ohne WebView2 faellt Rippy auf den Browser zurueck. Das ist weniger
schoen, aber es LAEUFT — ein Fehler waere hier eine Luege."""
befund = e.pruefe_webview(version="")
assert befund["zustand"] == e.WARNUNG
assert "Browser" in befund["abhilfe"]
def test_vorhandenes_webview_ist_ok():
assert e.pruefe_webview(version="151.0.4129.107")["zustand"] == e.OK
def test_zu_wenig_platz_ist_ein_fehler():
befund = e.pruefe_platz("C:\\egal", frei_mb=100)
assert befund["zustand"] == e.FEHLER
assert "500" in befund["text"]
def test_genug_platz_ist_ok():
assert e.pruefe_platz("C:\\egal", frei_mb=5000)["zustand"] == e.OK
def test_fehlendes_schreibrecht_ist_ein_fehler():
"""`os.access(..., W_OK)` luegt unter Windows — es beantwortet die Frage
anhand des Nur-Lesen-Flags, nicht anhand der Zugriffsrechte. Deshalb wird
wirklich geschrieben."""
befund = e.pruefe_schreibrecht("C:\\Programme\\Rippy",
probe=(False, "Zugriff verweigert"))
assert befund["zustand"] == e.FEHLER
assert "Benutzer" in befund["abhilfe"]
def test_schreibprobe_am_echten_ordner(tmp_path):
befund = e.pruefe_schreibrecht(str(tmp_path))
assert befund["zustand"] == e.OK
# Und die Probe darf nichts hinterlassen.
assert list(tmp_path.iterdir()) == []
def test_belegter_port_ist_nur_eine_warnung():
"""Sehr oft ist es ein bereits laufendes Rippy — und dann ist eine
Neuinstallation genau richtig."""
befund = e.pruefe_port(7788, belegt=True)
assert befund["zustand"] == e.WARNUNG
assert "Rippy" in befund["abhilfe"]
def test_freier_port_ist_ok():
assert e.pruefe_port(7788, belegt=False)["zustand"] == e.OK
def test_kein_laufwerk_ist_nur_eine_warnung():
"""install.sh sagt seit Langem: ohne Laufwerk „laeuft trotzdem durch, dann
ist das eine reine KOMPRIMIER-Maschine"."""
befund = e.pruefe_laufwerk(laufwerke=[])
assert befund["zustand"] == e.WARNUNG
assert "komprimieren" in befund["abhilfe"]
def test_gefundenes_laufwerk_wird_genannt():
befund = e.pruefe_laufwerk(laufwerke=["\\\\.\\G:"])
assert befund["zustand"] == e.OK
assert "G:" in befund["text"]
# ── Die Werkzeuge ───────────────────────────────────────────────────────
def _bestand(makemkv, handbrake):
return {
"makemkv": {"titel": "MakeMKV", "da": makemkv, "version": "1.18.4",
"pflicht": True},
"handbrake": {"titel": "HandBrakeCLI", "da": handbrake,
"version": "1.11.2", "pflicht": False},
}
def test_fehlendes_makemkv_ist_eine_warnung_mit_ausweg():
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(False, True))}
assert befunde["MakeMKV"]["zustand"] == e.WARNUNG
assert "makemkv.com" in befunde["MakeMKV"]["abhilfe"]
def test_fehlendes_handbrake_ist_kein_problem():
"""Es liegt im Paket — das Setup legt es beim Installieren daneben."""
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, False))}
assert befunde["HandBrakeCLI"]["zustand"] == e.OK
assert "mitgeliefert" in befunde["HandBrakeCLI"]["text"]
def test_vorhandene_werkzeuge_zeigen_ihre_version():
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, True))}
assert befunde["MakeMKV"]["text"] == "1.18.4"
# ── Die Zusammenfassung ─────────────────────────────────────────────────
def test_zusammenfassung_nennt_die_fehler_beim_namen():
text = e.zusammenfassung([_p(e.FEHLER, "Speicherplatz"), _p(e.OK, "Port")])
assert "nicht möglich" in text
assert "Speicherplatz" in text
def test_zusammenfassung_nennt_die_hinweise():
text = e.zusammenfassung([_p(e.WARNUNG, "MakeMKV"), _p(e.OK, "Port")])
assert "MakeMKV" in text
assert "nicht möglich" not in text
def test_alles_gruen_sagt_es_auch_so():
assert e.zusammenfassung([_p(e.OK), _p(e.OK)]) == "Alles bereit."
def test_jede_pruefung_hat_dieselbe_form():
"""Ein fehlendes Feld waere in der Oberflaeche ein `undefined`."""
for befund in ([e.pruefe_windows(22631), e.pruefe_webview(version=""),
e.pruefe_laufwerk(laufwerke=[]),
e.pruefe_platz("C:\\x", frei_mb=9999),
e.pruefe_port(7788, belegt=False)]
+ e.pruefe_werkzeuge(_bestand(False, False))):
assert set(befund) == {"name", "zustand", "text", "abhilfe"}, befund
assert befund["zustand"] in (e.OK, e.WARNUNG, e.FEHLER)
if befund["zustand"] != e.OK:
assert befund["abhilfe"], (
"%s sagt nicht, was zu tun ist" % befund["name"])
# ── Die Vorgaben ────────────────────────────────────────────────────────
def test_vorgaben_sind_vollstaendig():
if os.name != "nt":
import pytest
pytest.skip("Registry-Vorgaben gibt es nur unter Windows")
v = e.vorgaben()
assert set(v) == {"ziel", "ablage", "port", "autostart",
"verknuepfungen", "werkzeuge_holen"}
assert v["port"] == 7788
assert v["ziel"] and v["ablage"]
assert "/app/" not in v["ablage"], "Container-Pfad in einer Windows-Vorgabe"
+122
View File
@@ -0,0 +1,122 @@
"""Der Einrichtungs-Assistent: Bruecke und Seite, ohne ein Fenster zu oeffnen.
Ein Fenster laesst sich im Test nicht bedienen. Geprueft wird deshalb das,
was DAHINTER haengt: was die Seite abfragen darf, was beim Installieren
passiert, und dass ein Fehlschlag sichtbar wird statt zu verschwinden.
Hinweis zu den Backslashes: Windows-Pfade stehen hier in ROHEN Zeichenketten
(`r"..."`). In einer normalen waere `C:\\Rippy` die ungueltige Escape-Sequenz
Backslash-R.
"""
from rippy import setup_fenster
class LeeresFenster:
def __init__(self, antwort=None):
self.antwort = antwort
self.zerstoert = False
def create_file_dialog(self, *a, **k):
if self.antwort is None:
raise RuntimeError("kein Dialog")
return self.antwort
def destroy(self):
self.zerstoert = True
# ── Die Seite ───────────────────────────────────────────────────────────
def test_die_seite_fragt_nach_dem_ablageort():
"""Die ausdrueckliche Frage des Commanders: „wo will ich das
hinspeichern"."""
assert setup_fenster.seite_enthaelt('id="ablage"')
assert setup_fenster.seite_enthaelt("Fertige Filme")
def test_die_seite_zeigt_die_voraussetzungen():
"""„nen pre requirement check usw." — vorher kam da gar nichts."""
assert setup_fenster.seite_enthaelt("Voraussetzungen")
assert setup_fenster.seite_enthaelt("pywebview.api.pruefen")
def test_die_seite_laedt_nichts_aus_dem_netz():
"""Der Assistent muss auf einem Rechner OHNE Internet aufgehen — dort
wird er am dringendsten gebraucht."""
for muster in ("http://", "https://", "<script src", "<link "):
assert not setup_fenster.seite_enthaelt(muster), muster
def test_die_seite_kann_den_ordner_waehlen_lassen():
assert setup_fenster.seite_enthaelt("pywebview.api.ordner_waehlen")
# ── Die Bruecke ─────────────────────────────────────────────────────────
def test_abgebrochener_ordner_dialog_gibt_leer_zurueck():
b = setup_fenster.Bruecke()
b._fenster = LeeresFenster(antwort=None)
assert b.ordner_waehlen("C:") == ""
def test_gewaehlter_ordner_kommt_als_text():
b = setup_fenster.Bruecke()
b._fenster = LeeresFenster(antwort=(r"D:\Filme",))
assert b.ordner_waehlen() == r"D:\Filme"
def test_fortschritt_leert_sich_beim_abholen():
"""Sonst waechst das Protokoll bei jedem Abruf um alles Bisherige."""
b = setup_fenster.Bruecke()
b._fortschritt = ["eins", "zwei"]
assert b.fortschritt() == ["eins", "zwei"]
assert b.fortschritt() == []
def test_stand_ist_leer_solange_nichts_fertig_ist():
assert setup_fenster.Bruecke().stand() == {}
def test_ein_fehlschlag_beim_installieren_wird_SICHTBAR(monkeypatch):
"""Ohne Konsole waere ein Absturz hier voellig unsichtbar — der Nutzer
saehe ein Fenster, in dem nichts mehr passiert."""
from rippy import windows_app
def platzt(**k):
raise OSError("Zugriff verweigert")
monkeypatch.setattr(windows_app, "installieren", platzt)
b = setup_fenster.Bruecke()
b._installieren_wirklich({"ziel": r"C:\x", "werkzeuge_holen": False})
assert b.ergebnis["ok"] is False
assert "Zugriff verweigert" in b.ergebnis["fehler"]
assert any("Fehlgeschlagen" in z for z in b._fortschritt)
def test_erfolgreiche_installation_meldet_den_ordner(monkeypatch):
from rippy import windows_app
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
b = setup_fenster.Bruecke()
b._installieren_wirklich({"ziel": r"C:\Rippy", "werkzeuge_holen": False})
assert b.ergebnis == {"ok": True, "ziel": r"C:\Rippy", "bereit": True}
def test_werkzeuge_koennen_uebersprungen_werden(monkeypatch):
"""Sonst wartete eine unbeaufsichtigte Installation auf einen Download."""
from rippy import windows_app
from rippy.tools import einrichten
def haette_nicht(**k):
raise AssertionError("haette nicht laufen duerfen")
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
monkeypatch.setattr(einrichten, "sicherstellen", haette_nicht)
b = setup_fenster.Bruecke()
b._installieren_wirklich({"werkzeuge_holen": False})
assert b.ergebnis["ok"] is True
def test_schliessen_ohne_fenster_wirft_nicht():
setup_fenster.Bruecke().schliessen()
+30 -3
View File
@@ -121,10 +121,37 @@ def ordner_von(datei: str) -> str:
def _entfalten(pfad: str, umgebung: dict) -> str: def _entfalten(pfad: str, umgebung: dict) -> str:
"""%ProgramFiles%\\X -> C:\\Program Files\\X (mit eigener Umgebung testbar).""" r"""%ProgramFiles%\X -> C:\Program Files\X (mit eigener Umgebung testbar).
## Warum das GROSS- UND KLEINSCHREIBUNG ignorieren muss (28.08.2026)
Windows behandelt Umgebungsvariablen ohne Rücksicht auf die Schreibweise,
und Python speichert sie unter Windows deshalb GROSS in `os.environ`.
Gemessen auf dem Rechner des Commanders:
os.environ PROGRAMFILES(X86) = C:\Program Files (x86)
WERKZEUGE["orte"] %ProgramFiles(x86)%\MakeMKV
Der Vergleich schlug fehl, `%` blieb stehen, und die Funktion gab ""
der Kandidat fiel aus der Liste. **Die bekannten Installationsorte haben
auf einer echten Maschine also nie funktioniert.** MakeMKV wurde nur über
die Registry-Rückfallebene gefunden; wo die fehlt oder anders aussieht,
fand Rippy nichts.
Aufgefallen ist es nur, weil hier eine ECHTE Umgebung eingesetzt wurde.
Der Test spritzte `{"ProgramFiles(x86)": …}` in genau der Schreibweise
ein, die im Muster steht — er war damit grüner als die Wirklichkeit.
"""
gross = {("%" + name + "%").upper(): wert for name, wert in umgebung.items()}
ergebnis = pfad ergebnis = pfad
for name, wert in umgebung.items(): for marke, wert in gross.items():
ergebnis = ergebnis.replace("%" + name + "%", wert) # Suchen ohne Rücksicht auf die Schreibweise, ersetzen an der
# gefundenen Stelle — `str.replace` kann das nicht von allein.
while True:
stelle = ergebnis.upper().find(marke)
if stelle < 0:
break
ergebnis = ergebnis[:stelle] + wert + ergebnis[stelle + len(marke):]
return ergebnis if "%" not in ergebnis else "" return ergebnis if "%" not in ergebnis else ""
+37 -2
View File
@@ -17,9 +17,17 @@ from rippy.tools import katalog
BACKSLASH = chr(92) BACKSLASH = chr(92)
# Eine Umgebung ohne echte Systemvariablen, damit die Pfade vorhersagbar sind. # Eine Umgebung ohne echte Systemvariablen, damit die Pfade vorhersagbar sind.
#
# ⚠️ Die Schluessel stehen GROSS — genau so, wie Windows sie in `os.environ`
# ablegt (am 28.08.2026 nachgesehen: `PROGRAMFILES(X86)`). Vorher stand hier
# die huebsche Schreibweise `ProgramFiles(x86)`, und damit war dieser Test
# gruener als die Wirklichkeit: Die Entfaltung verglich Zeichen fuer Zeichen,
# fand auf einer echten Maschine nie eine Uebereinstimmung und lieferte "".
# Die bekannten Installationsorte haben deshalb NIE funktioniert; MakeMKV
# wurde nur ueber die Registry-Rueckfallebene gefunden.
UMGEBUNG = { UMGEBUNG = {
"ProgramFiles": r"C:\Program Files", "PROGRAMFILES": r"C:\Program Files",
"ProgramFiles(x86)": r"C:\Program Files (x86)", "PROGRAMFILES(X86)": r"C:\Program Files (x86)",
} }
@@ -222,3 +230,30 @@ def test_programmdatei_aus_beiden_formen():
assert p(r"C:\R\x.exe") == r"C:\R\x.exe" assert p(r"C:\R\x.exe") == r"C:\R\x.exe"
assert p("") == "" assert p("") == ""
assert p("irgendwas ohne Programm") == "" assert p("irgendwas ohne Programm") == ""
def test_schreibweise_der_umgebungsvariablen_ist_egal():
"""DER Fehler vom 28.08.2026. Windows legt Umgebungsvariablen GROSS ab,
das Muster in WERKZEUGE steht huebsch geschrieben. Ohne diese Pruefung
faellt der Unterschied nie auf — der alte Test spritzte genau die
Schreibweise ein, die im Muster stand."""
for schreibweise in ("ProgramFiles(x86)", "PROGRAMFILES(X86)",
"programfiles(x86)"):
entfaltet = katalog._entfalten(
r"%ProgramFiles(x86)%\MakeMKV", {schreibweise: r"C:\PF86"})
assert entfaltet == r"C:\PF86\MakeMKV", (schreibweise, entfaltet)
def test_mit_der_ECHTEN_umgebung_kommen_die_systempfade_heraus():
"""Die Gegenprobe an os.environ statt an einer gepflegten Attrappe.
Genau die hat gefehlt."""
import os
if os.name != "nt":
import pytest
pytest.skip("ProgramFiles gibt es nur unter Windows")
pfade = katalog.kandidaten("makemkv", r"C:\Rippy ools", os.environ)
assert any("Program Files" in p for p in pfade), (
"Kein einziger Systempfad in der Kandidatenliste — dann findet Rippy "
"eine normale MakeMKV-Installation nur noch ueber die Registry."
)
+47 -1
View File
@@ -593,6 +593,7 @@ Optionen:
--kein-autostart --kein-autostart
--keine-verknuepfungen ohne Desktop-Symbol und Startmenue-Eintrag --keine-verknuepfungen ohne Desktop-Symbol und Startmenue-Eintrag
--ohne-werkzeuge MakeMKV/HandBrake NICHT pruefen und nicht holen --ohne-werkzeuge MakeMKV/HandBrake NICHT pruefen und nicht holen
--ohne-assistent ohne Einrichtungs-Fenster, mit den Vorgaben
--im-browser im Browser statt im eigenen Fenster --im-browser im Browser statt im eigenen Fenster
--still ohne Rückmeldungen --still ohne Rückmeldungen
""" """
@@ -636,7 +637,27 @@ def main(argv=None) -> int:
if "--dienst" in argv: if "--dienst" in argv:
return dienst_starten(int(wert("--port", STANDARD_PORT))) return dienst_starten(int(wert("--port", STANDARD_PORT)))
# Ohne Argument: installieren. Das ist der Doppelklick-Fall. # Ohne Argument: der Doppelklick-Fall — und der gehoert in den
# Einrichtungs-Assistenten.
#
# Commander am 28.08.2026: "Dann haette ich beim 'Setup' auch erwartet das
# nen echtes setup passiert - wo will ich das hinspeichern, nen pre
# requirement check usw. Da kommt garnichts Rippy geht einfach auf."
#
# `--still` und `--ohne-assistent` gehen weiterhin den stummen Weg: fuer
# unbeaufsichtigte Installationen und fuer die eigenen Tests.
if "--still" not in argv and "--ohne-assistent" not in argv:
ergebnis = assistent_starten()
if ergebnis.get("ok"):
return 0
if ergebnis.get("abgebrochen"):
melden("Abgebrochen — es wurde nichts veraendert.")
return 1
# Weder erledigt noch abgebrochen: Der Assistent ging gar nicht auf.
# Dann wird ohne ihn installiert, statt gar nichts zu tun.
melden("Der Einrichtungs-Assistent ging nicht auf (%s) — es wird mit "
"den Vorgaben installiert." % (ergebnis.get("grund") or "?"))
try: try:
ziel = installieren( ziel = installieren(
ziel=wert("--ziel"), ziel=wert("--ziel"),
@@ -671,6 +692,31 @@ def main(argv=None) -> int:
return 0 return 0
def assistent_starten() -> dict:
"""Oeffnet den Einrichtungs-Assistenten. Sagt, was daraus wurde.
Rueckgabe:
{"ok": True, ...} installiert
{"abgebrochen": True} Fenster zugemacht, nichts veraendert
{"grund": "..."} ging nicht auf — der Aufrufer entscheidet
Wirft nicht. Ein Setup, das gar nichts tut, weil sein Fenster nicht
aufging, waere schlimmer als eines, das nicht fragt.
"""
from rippy import setup_fenster
geht, grund = setup_fenster.moeglich()
if not geht:
return {"grund": grund}
try:
ergebnis = setup_fenster.starten()
except Exception as e: # noqa: BLE001
return {"grund": "%s: %s" % (type(e).__name__, e)}
if not ergebnis:
return {"abgebrochen": True}
return ergebnis
def werkzeuge_einrichten() -> str: def werkzeuge_einrichten() -> str:
"""Holt fehlende Werkzeuge und gibt den Bericht als Text zurück. """Holt fehlende Werkzeuge und gibt den Bericht als Text zurück.