#!/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. NACHTRAG (real beim Testen entdeckt, 29.08.2026): auf der manuellen Partitionierungsseite ("Etwas Anderes") lief der Klick auf "Jetzt installieren" nachweislich zuverlaessig (AT-SPI meldete jedes Mal Erfolg), aber die Seite kam nie voran - stattdessen loeste jeder Klick erneut die Debconf-Frage "ubiquity/partman-rebuild-cache" aus (/lib/partman/update.d/99signal_ubiquity), was ubi-partman.py zurueck auf dieselbe Seite wirft (siehe ADR-0023-Nachtrag). Ein gezielter Fix am Partitionsrezept (kein zusaetzliches $bootable{ } auf der Root-Partition unter UEFI) aenderte NICHTS - das deutet auf einen zeitlichen statt strukturellen Fehler hin: die Seite zeigt waehrend dieser Phase ein Status-Panel "Konfiguration der Installation wird ueberprueft ..." - klickt der Autoklicker WAEHREND dieser Hintergrundpruefung noch laeuft, koennte er sie unterbrechen/neu anstossen, statt ihr Zeit zum Abschliessen zu geben. Dieses Skript wartet deshalb jetzt aktiv, solange dieser Text sichtbar ist (siehe VALIDATION_TEXT_MARKERS/find_showing_text()), und klickt erst, wenn er verschwunden ist. 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), 3 = Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar - bewusst nicht geklickt, siehe Nachtrag oben. """ 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 # Zweisprachig vorsichtshalber (siehe Multimedia-Codecs-Erfahrung oben), # auch wenn dieser konkrete Text bisher immer deutsch beobachtet wurde. VALIDATION_TEXT_MARKERS = ( "wird überprüft", "wird ueberprueft", "being checked", "checking the install", ) def _node_text(node): """Sammelt allen aus einem Accessible auslesbaren Text: .name, .description UND (falls unterstuetzt) das separate Text-Interface. WICHTIG (real beim Testen entdeckt, 29.08.2026): ein einfaches GtkLabel exponiert seinen sichtbaren Text ueblicherweise NICHT ueber .name/ .description (die bleiben dort oft leer) sondern ausschliesslich ueber das AT-SPI-Text-Interface (queryText().getText(0, -1)) - ohne diesen dritten Zugriffsweg fand find_showing_text() den sichtbar angezeigten Status-Text nie, obwohl er laut Screenshot die ganze Zeit auf dem Schirm stand.""" parts = [] try: if node.name: parts.append(node.name) except Exception: pass try: if node.description: parts.append(node.description) except Exception: pass try: text_iface = node.queryText() content = text_iface.getText(0, -1) if content: parts.append(content) except NotImplementedError: pass except Exception: pass return " ".join(parts).strip().lower() def find_showing_text(node, needle, depth=0): """Sucht ein sichtbares Accessible, dessen Text (siehe _node_text()) den Suchtext enthaelt - unabhaengig von seiner Rolle (Label/Expander/...), da Ubiquitys Status-Panel-Text nicht zuverlaessig unter derselben Rolle exponiert sein muss. Bei Unsicherheit ueber den Sichtbarkeitsstatus bewusst konservativ (lieber einen Zyklus zu viel warten als einen Klick mitten in die Pruefung setzen).""" if node is None or depth > MAX_DEPTH: return False if needle in _node_text(node): try: state = node.getState() return state.contains(pyatspi.STATE_SHOWING) except Exception: return True try: count = node.childCount except Exception: return False for i in range(count): try: child = node.getChildAtIndex(i) except Exception: continue if find_showing_text(child, needle, depth + 1): return True return False 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 apps = [] for i in range(desktop.childCount): try: app = desktop.getChildAtIndex(i) except Exception: continue if app is not None: apps.append(app) for marker in VALIDATION_TEXT_MARKERS: for app in apps: if find_showing_text(app, marker): print( f"Hintergrundpruefung noch sichtbar ('{marker}'), warte statt zu klicken.", file=sys.stderr, ) return 3 all_buttons = [] for app in apps: 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())