Thomas Stallinger 6e99b3bf3a fix: atspi_click.py sprachunabhaengig statt reine deutsche Werteliste
Real beim Testen entdeckt: die Mint-eigene 'Multimedia-Codecs'-Seite zeigt
trotz debian-installer/language=de englische Buttons ('Quit'/'Back'/
'Continue') - vermutlich derselbe fehlende .mo-Uebersetzungskatalog wie
beim bereits gepatchten ubi-prepare.py-None-Bug. Die deutsche
FORWARD_LABELS-Werteliste haette hier fuer immer haengen bleiben.

Neue Strategie in select_target(): bekannte rueckwaerts-/abbrechende
Beschriftungen (zweisprachig) werden ausgeschlossen, danach wird bevorzugt
wer eine bekannte vorwaerts-Beschriftung traegt, sonst der einzige
verbleibende Kandidat (Sole-Survivor) - deckt auch unbekannte/nicht
uebersetzte Labels ab. Bei echter Mehrdeutigkeit (mehrere unbekannte
Kandidaten gleichzeitig) wird bewusst NICHT geklickt statt zu raten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 15:42:55 +02:00

207 lines
6.9 KiB
Python
Executable File

#!/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())