#!/usr/bin/env python3 """Klickt den primaeren "Weiter"-Button der aktuell sichtbaren Ubiquity-Seite per AT-SPI (Linux-Barrierefreiheits-API) - wird von autoclicker.sh wiederholt aufgerufen (ein Prozessstart pro Klickversuch, absichtlich einfach statt dauerhaft im Hintergrund am Accessibility-Bus zu lauschen). Warum AT-SPI statt Bildschirmkoordinaten: der urspruengliche xdotool-Ansatz scheiterte daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - eine "groesstes sichtbares Fenster"-Heuristik traf wiederholt das falsche Fenster (siehe ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber seine Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung selbst - unabhaengig von Fenstergeometrie/-dekoration. WICHTIG (real beim Testen entdeckt, 29.08.2026): trotz fest gesetztem "debian-installer/language=de" sind NICHT alle Assistentenseiten auf Deutsch beschriftet - die Mint-eigene "Multimedia-Codecs"-Seite zeigt reale englische Buttons ("Quit"/"Back"/"Continue"), vermutlich dieselbe Ursache wie der bereits gepatchte ubi-prepare.py-None-Bug (fehlender/unvollstaendiger .mo-Uebersetzungskatalog fuer dieses Plugin auf dem Live-Medium). Eine reine Werteliste bekannter deutscher Beschriftungen (fruehere Version dieses Skripts) haette hier fuer immer haengen bleiben. Die Logik wurde deshalb auf eine sprachunabhaengige Ausschluss-Strategie umgestellt: bekannte rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden aus der Kandidatenliste entfernt, der bzw. die verbleibenden Kandidaten werden geklickt - siehe select_target() fuer die genaue Regel samt Sicherheitsverhalten bei Mehrdeutigkeit. Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed grundsaetzlich auf de_DE gesetzt (siehe backend.sh), s.o. aber nicht verlaesslich fuer jede einzelne Seite. Exit-Codes: 0 = ein Button wurde gefunden und geklickt, 1 = kein eindeutiger Button gefunden (z.B. Seite im Ladezustand, oder echte Mehrdeutigkeit - dann bewusst NICHT geklickt, siehe select_target()), 2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv). """ import sys try: import pyatspi except ImportError: print("pyatspi nicht verfuegbar", file=sys.stderr) sys.exit(2) # Zweisprachig, da nicht jede Seite verlaesslich deutsch beschriftet ist # (siehe Modul-Docstring). Diese Namen werden aus der Kandidatenliste # AUSGESCHLOSSEN, nie geklickt - unabhaengig davon, ob sie der einzige # sichtbare Button waeren. EXCLUDE_LABELS = { "zurück", "zurueck", "back", "abbrechen", "abbruch", "cancel", "beenden", "quit", "exit", "verwerfen", } # Bevorzugt geklickt, falls mehrere Kandidaten nach dem Ausschluss uebrig # bleiben (siehe select_target()). Kein Ausschlusskriterium fuer # Kandidaten, die HIER nicht drin stehen - die duerfen trotzdem als # alleiniger Kandidat geklickt werden (Sole-Survivor-Regel), damit auch # unbekannte/nicht uebersetzte Beschriftungen wie das reale "Continue" # oben nicht zum Stillstand fuehren. PREFER_LABELS = { "weiter", "continue", "next", "jetzt installieren", "install now", "fortfahren", "proceed", "installieren", "install", "neu starten", "restart now", "schließen", "schliessen", "close", "finish", "fertigstellen", "done", } MAX_DEPTH = 40 def collect_buttons(node, depth=0, acc=None): """Alle sichtbaren+aktivierten Push-Buttons im Baum einsammeln (Rolle, Zustand). Absichtlich ALLE Kandidaten sammeln statt beim ersten Treffer abzubrechen - select_target() braucht die vollstaendige Liste, um Mehrdeutigkeit erkennen zu koennen.""" if acc is None: acc = [] if node is None or depth > MAX_DEPTH: return acc try: role = node.getRole() except Exception: return acc if role == pyatspi.ROLE_PUSH_BUTTON: try: state = node.getState() if state.contains(pyatspi.STATE_SENSITIVE) and state.contains( pyatspi.STATE_SHOWING ): acc.append(node) except Exception: pass return acc try: count = node.childCount except Exception: return acc for i in range(count): try: child = node.getChildAtIndex(i) except Exception: continue collect_buttons(child, depth + 1, acc) return acc def select_target(buttons): """Waehlt aus allen sichtbaren+aktivierten Buttons genau einen aus, der geklickt werden darf - oder None, wenn das nicht sicher moeglich ist. Regel (bewusst sicherheitsorientiert, siehe Modul-Docstring): 1. Bekannte rueckwaerts-/abbrechende Beschriftungen (EXCLUDE_LABELS) werden nie geklickt, unabhaengig vom Rest. 2. Von den verbleibenden Kandidaten wird bevorzugt, wer eine bekannte vorwaerts-/abschliessende Beschriftung traegt (PREFER_LABELS). 3. Bleibt nach 1./2. KEIN bekannter Treffer, aber genau EIN Kandidat uebrig, wird dieser geklickt (Sole-Survivor-Regel - deckt unbekannte/ nicht uebersetzte Beschriftungen wie das real beobachtete "Continue" ab, siehe Modul-Docstring). 4. Bleiben mehrere unbekannte Kandidaten gleichzeitig uebrig, wird NICHT geklickt (echte Mehrdeutigkeit - ein Fehlklick waere hier riskanter als ein uebersprungener Zyklus, die Schleife in autoclicker.sh versucht es 2 Sekunden spaeter erneut).""" candidates = [] for btn in buttons: name = (btn.name or "").strip().lower() if name in EXCLUDE_LABELS: continue candidates.append(btn) if not candidates: return None preferred = [b for b in candidates if (b.name or "").strip().lower() in PREFER_LABELS] if preferred: return preferred[0] if len(candidates) == 1: return candidates[0] return None def main(): try: desktop = pyatspi.Registry.getDesktop(0) except Exception as exc: print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr) return 2 all_buttons = [] for i in range(desktop.childCount): try: app = desktop.getChildAtIndex(i) except Exception: continue if app is None: continue collect_buttons(app, 0, all_buttons) target = select_target(all_buttons) if target is None: names = [repr((b.name or "").strip()) for b in all_buttons] print( "kein eindeutiger Button gefunden, sichtbare Buttons: " + (", ".join(names) if names else "(keine)"), file=sys.stderr, ) return 1 try: action = target.queryAction() action.doAction(0) except Exception as exc: print(f"Klick auf '{target.name}' fehlgeschlagen: {exc}", file=sys.stderr) return 1 print(f"geklickt: {target.name}") return 0 if __name__ == "__main__": sys.exit(main())