kimal3 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,17 @@ UMSCHALTER = {"ctrl": "Strg", "ctrl_l": "Strg", "ctrl_r": "Strg", "alt": "Alt",
43
43
  "cmd": "Win", "cmd_l": "Win", "cmd_r": "Win", "shift": "Umschalt", "shift_l": "Umschalt",
44
44
  "shift_r": "Umschalt", "alt_gr": "AltGr"}
45
45
  KOMBI = ("Strg", "Alt", "Win")
46
+ # Mac (Aufnehmer macOS, input_observer/darwin.py `_KEY_NAMES`): In `key_name` -- der Spalte, die wir lesen -- stehen die
47
+ # ROHEN Namen mit Seite (cmd_l/cmd_r, alt_l/alt_r, shift_l/shift_r, ctrl_l/ctrl_r); vereinheitlicht ist nur
48
+ # `canonical_key_name`. Beide Formen gelten, damit auch eine Aufnahme mit vereinheitlichten Namen stimmt.
49
+ # `fn` und `caps_lock` kommen als eigene Tasten und sind weder Text noch Kombination.
50
+ MAC_UMSCHALTER = {name + seite: zeichen for name, zeichen in {"cmd": "⌘", "alt": "⌥", "ctrl": "⌃", "shift": "⇧"}.items()
51
+ for seite in ("", "_l", "_r")}
52
+ MAC_REIHENFOLGE = ("⌃", "⌥", "⇧", "⌘") # wie das Mac-Menue: Ctrl, Option, Umschalt, Befehl
53
+ MAC_KOMBI = ("⌃", "⌥", "⌘") # nur diese machen eine Kombination; Umschalt allein ist Grossschreibung
54
+ MAC_STUMM = ("fn", "caps_lock")
55
+ MAC_PFEILE = {"up": "↑", "down": "↓", "left": "←", "right": "→"}
56
+ MAC_EINGABEFELDER = ("edit", "axtextfield", "axtextarea", "axcombobox", "axsearchfield")
46
57
  ZEICHEN = {"space": " ", "enter": "⏎", "tab": "⇥", "up": "↑", "down": "↓", "left": "←", "right": "→"}
47
58
  # Passwortfelder: Windows meldet `IsPassword`, gespeichert wird das Merkmal aber nicht (StructuralElement kennt kein
48
59
  # Feld dafuer, structural.py). Deshalb wie der Aufnehmer selbst (shadow/events.py native_field_is_secure) zusaetzlich
@@ -66,7 +77,7 @@ def _woerter_teilen(text: str) -> str:
66
77
  return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", re.sub(r"[-_.#]+", " ", text))
67
78
 
68
79
 
69
- def passwortfeld(beobachtung: dict | None) -> bool:
80
+ def passwortfeld(beobachtung: dict | None, plattform: str = "win32") -> bool:
70
81
  if not isinstance(beobachtung, dict):
71
82
  return False
72
83
  e = beobachtung.get("element") or {}
@@ -76,7 +87,8 @@ def passwortfeld(beobachtung: dict | None) -> bool:
76
87
  if "password" in rollen or "secure" in rollen:
77
88
  return True
78
89
  namen = [e.get("automation_id"), e.get("class_name")]
79
- if str(e.get("control_type") or e.get("role") or "").casefold() == "edit":
90
+ felder = MAC_EINGABEFELDER if plattform == "darwin" else ("edit",)
91
+ if str(e.get("control_type") or e.get("role") or "").casefold() in felder:
80
92
  namen.append(e.get("name"))
81
93
  return any(isinstance(n, str) and GEHEIM.search(_woerter_teilen(n)) for n in namen)
82
94
 
@@ -86,11 +98,14 @@ def _programm(beobachtung: dict) -> str | None:
86
98
  return re.sub(r"\.exe$", "", name, flags=re.IGNORECASE).casefold() or None
87
99
 
88
100
 
89
- def _klick_text(beobachtung: dict | None, t: dict) -> str:
101
+ def _klick_text(beobachtung: dict | None, t: dict, plattform: str = "win32") -> str:
90
102
  if not isinstance(beobachtung, dict) or not beobachtung.get("element"):
91
103
  return t["unbekannt"]
92
104
  e = beobachtung["element"]
93
- teile = [_eine_zeile(e.get("role") or e.get("control_type"))]
105
+ rolle = _eine_zeile(e.get("role") or e.get("control_type"))
106
+ if plattform == "darwin":
107
+ rolle = re.sub(r"^AX(?=[A-Z])", "", rolle) # AXLink -> Link
108
+ teile = [rolle]
94
109
  if _eine_zeile(e.get("name")):
95
110
  teile.append(f"{t['auf']}{_eine_zeile(e['name'])}{t['zu']}")
96
111
  programm = _programm(beobachtung)
@@ -118,7 +133,9 @@ def _saetze(woerter: list) -> list:
118
133
  return saetze + ([satz] if satz else [])
119
134
 
120
135
 
121
- def _kombi_name(e: dict) -> str:
136
+ def _kombi_name(e: dict, plattform: str = "win32") -> str:
137
+ if plattform == "darwin" and e.get("taste") in MAC_PFEILE:
138
+ return MAC_PFEILE[e["taste"]]
122
139
  zeichen = e.get("zeichen") or ""
123
140
  if len(zeichen) == 1 and 1 <= ord(zeichen) <= 26: # Strg+Buchstabe kommt als Steuerzeichen: \x03 = Strg+C
124
141
  return chr(64 + ord(zeichen))
@@ -127,11 +144,15 @@ def _kombi_name(e: dict) -> str:
127
144
  return (e.get("taste") or "?").replace("_", " ").title()
128
145
 
129
146
 
130
- def zeitleiste_aus(kopf: dict, woerter: list, ereignisse: list, sprache: str, ohne_getipptes: bool) -> tuple:
147
+ def zeitleiste_aus(kopf: dict, woerter: list, ereignisse: list, sprache: str, ohne_getipptes: bool,
148
+ plattform: str = "win32") -> tuple:
131
149
  """Text der Zeitleiste und ihre Zahlen. kopf: aufgabe, datum (fertiger Text), dauer_sek. woerter: (Anfang, Ende, Wort)
132
150
  in Sekunden ab Aufnahmestart. ereignisse: dicts mit t, art (click|press|release|…), beobachtung, bei Tasten
133
- taste und zeichen."""
151
+ taste und zeichen. plattform: `recording.platform` der Aufnahme, "darwin" = Mac, sonst wie Windows."""
134
152
  t = TEXTE[sprache]
153
+ mac = plattform == "darwin"
154
+ umschalter = MAC_UMSCHALTER if mac else UMSCHALTER
155
+ kombi_reihe = MAC_KOMBI if mac else KOMBI
135
156
  zahlen = {"saetze": 0, "klicks": 0, "tippstellen": 0, "passwortfelder": 0}
136
157
  zeilen = [] # (Sekunde, Verb, Text)
137
158
 
@@ -161,27 +182,36 @@ def zeitleiste_aus(kopf: dict, woerter: list, ereignisse: list, sprache: str, oh
161
182
  if art == "click":
162
183
  folge_ende()
163
184
  gehalten.clear()
164
- klick_geheim = passwortfeld(e.get("beobachtung"))
165
- zeilen.append((e["t"], t["klickt"], _klick_text(e.get("beobachtung"), t)))
185
+ klick_geheim = passwortfeld(e.get("beobachtung"), plattform)
186
+ zeilen.append((e["t"], t["klickt"], _klick_text(e.get("beobachtung"), t, plattform)))
166
187
  zahlen["klicks"] += 1
167
188
  continue
168
189
  if art == "release":
169
- gehalten.discard(UMSCHALTER.get(taste))
190
+ gehalten.discard(umschalter.get(taste))
170
191
  continue
171
192
  if art != "press":
172
193
  continue # Mausbewegung, Scrollen
173
- if taste in UMSCHALTER:
174
- gehalten.add(UMSCHALTER[taste])
194
+ if mac and taste in MAC_STUMM:
195
+ continue
196
+ if taste in umschalter:
197
+ gehalten.add(umschalter[taste])
175
198
  continue
176
199
  zeichen = e.get("zeichen") or ""
177
200
  steuer = taste is None and len(zeichen) == 1 and 1 <= ord(zeichen) <= 26 # Tab/Enter haben einen Namen
178
201
  # Windows meldet AltGr als ctrl_l + alt_r, also Strg+Alt: ein druckbares Zeichen dabei ist Text („@", „€").
179
202
  altgr = bool(zeichen) and zeichen.isprintable() and ("AltGr" in gehalten or {"Strg", "Alt"} <= gehalten)
203
+ if mac: # Option (auch mit Umschalt) + druckbares Zeichen ist Text („@" auf Option+L); mit Befehl oder Ctrl nicht
204
+ altgr = bool(zeichen) and zeichen.isprintable() and "⌥" in gehalten and not {"⌘", "⌃"} & gehalten
180
205
  # Im Passwortfeld wird keine Kombination eine eigene Zeile: sie verriete Tasten und teilte die Ausblendung.
181
- geheim = klick_geheim or (folge is not None and folge["geheim"]) or passwortfeld(e.get("beobachtung"))
182
- kombi = [m for m in KOMBI if m in gehalten] or (["Strg"] if steuer else [])
206
+ geheim = klick_geheim or (folge is not None and folge["geheim"]) or passwortfeld(e.get("beobachtung"), plattform)
207
+ kombi = [m for m in kombi_reihe if m in gehalten] or ([("⌃" if mac else "Strg")] if steuer else [])
183
208
  if kombi and not altgr and not geheim:
184
209
  folge_ende()
210
+ if mac: # Mac-Schreibweise ohne Plus: [⌘C], [⌥→]; Umschalt kommt nur dazu, wenn schon eine Kombination da ist
211
+ kombi = [m for m in MAC_REIHENFOLGE if m in kombi or (m == "⇧" and m in gehalten)]
212
+ zeilen.append((e["t"], t["tippt"], "[" + "".join(kombi) + _kombi_name(e, plattform) + "]"))
213
+ gehalten.clear()
214
+ continue
185
215
  mods = kombi + (["Umschalt"] if "Umschalt" in gehalten else [])
186
216
  zeilen.append((e["t"], t["tippt"], "[" + "+".join(t.get(m, m) for m in mods) + f"+{_kombi_name(e)}]"))
187
217
  gehalten.clear() # eine Kombination verbraucht die Umschalter; die naechste Taste kommt wieder mit eigenen
@@ -192,7 +222,7 @@ def zeitleiste_aus(kopf: dict, woerter: list, ereignisse: list, sprache: str, oh
192
222
  if folge is None:
193
223
  folge = {"t": e["t"], "text": "", "geheim": klick_geheim, "zuletzt": e["t"]}
194
224
  folge["zuletzt"] = e["t"]
195
- folge["geheim"] = folge["geheim"] or passwortfeld(e.get("beobachtung"))
225
+ folge["geheim"] = folge["geheim"] or passwortfeld(e.get("beobachtung"), plattform)
196
226
  if taste == "backspace":
197
227
  folge["text"] = folge["text"][:-1]
198
228
  elif zeichen and zeichen.isprintable():
@@ -209,6 +239,18 @@ def zeitleiste_aus(kopf: dict, woerter: list, ereignisse: list, sprache: str, oh
209
239
 
210
240
  # --- Aufnahme lesen, Ton in Woerter ------------------------------------------------------------------------------
211
241
 
242
+ def _lies_plattform(ordner: str) -> str:
243
+ """`recording.platform` ("darwin" auf dem Mac). Aeltere Aufnahmen ohne Spalte oder mit leerem Wert: "win32"."""
244
+ db = sqlite3.connect(f"file:{os.path.join(ordner, ROH, 'recording.db')}?mode=ro", uri=True)
245
+ try:
246
+ wert = db.execute("select platform from recording").fetchone()
247
+ except sqlite3.OperationalError: # keine Spalte
248
+ return "win32"
249
+ finally:
250
+ db.close()
251
+ return wert[0] if wert and wert[0] else "win32"
252
+
253
+
212
254
  def _lies_aufnahme(ordner: str):
213
255
  db = sqlite3.connect(f"file:{os.path.join(ordner, ROH, 'recording.db')}?mode=ro", uri=True)
214
256
  try:
@@ -307,6 +349,7 @@ def auswerten(ordner: str, sprache: str, ohne_getipptes: bool, modell_da) -> dic
307
349
  return dict(daten, grund="keine-aufnahme")
308
350
  try:
309
351
  start, beschreibung, ereignisse, letzte, ton = _lies_aufnahme(ordner)
352
+ plattform = _lies_plattform(ordner)
310
353
  except (sqlite3.Error, TypeError, ValueError):
311
354
  return dict(daten, grund="aufnahme-unlesbar")
312
355
  daten["dauer_sek"] = round(max(letzte, 0.0), 2)
@@ -321,7 +364,7 @@ def auswerten(ordner: str, sprache: str, ohne_getipptes: bool, modell_da) -> dic
321
364
  dauer = max(letzte, ton_ende)
322
365
  kopf = {"aufgabe": _aufgabe(ordner, beschreibung), "datum": time.strftime("%Y-%m-%d %H:%M", time.localtime(start)),
323
366
  "dauer_sek": dauer}
324
- text, zahlen = zeitleiste_aus(kopf, woerter, ereignisse, sprache, ohne_getipptes)
367
+ text, zahlen = zeitleiste_aus(kopf, woerter, ereignisse, sprache, ohne_getipptes, plattform)
325
368
  daten.update(zahlen, dauer_sek=round(dauer, 2))
326
369
  if zahlen["saetze"] == 0:
327
370
  return dict(daten, ergebnis="keine-sprache")