tyrell.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. from os import name as _OS_NAME, environ as _env
  2. from os import listdir as _listdir, mkdir as _mkdir, chown as _chown
  3. from os.path import exists as _exists, join as _join, isdir as _isdir
  4. from copy import copy as _copy
  5. from time import sleep as _sleep
  6. from typing import Dict as _Dict, List as _List, Optional as _Option, Any as _Any
  7. VERSION: str = '1.0-rel2'
  8. try:
  9. from click import echo as _echo, style as _style, command as _command, option as _option, argument as _arg
  10. except ImportError:
  11. print("ERROR Please activate a virtual environment and install requirements.txt")
  12. exit()
  13. def _print_err(msg: str):
  14. _echo(_style("ERROR", fg="bright_red") + " " + msg)
  15. def _print_warn(msg: str):
  16. _echo(_style("WARN ", fg="bright_yellow") + " " + msg)
  17. def _print_ok(msg: str):
  18. _echo(_style(" OK ", fg="bright_green") + " " + msg)
  19. def _print_on(msg: str):
  20. _echo(_style(" ON ", fg="bright_green") + " " + msg)
  21. def _print_off(msg: str):
  22. _echo(_style(" OFF ", fg="bright_red") + " " + msg)
  23. def _print_info(msg: str):
  24. print(" " + msg)
  25. from keyboard import press as _key_down, release as _key_up, KeyboardEvent as _KeyboardEvent, hook as _key_hook, unhook as _key_unhook
  26. platform: str = ""
  27. os_name = _OS_NAME.lower()
  28. if "linux" in os_name or "posix" in os_name:
  29. platform = "LINUX"
  30. elif "windows" in os_name or "nt" in os_name:
  31. platform = "WINDOWS"
  32. elif "darwin" in os_name:
  33. platform = "MACOS"
  34. else:
  35. _print_err("Platform 'Linux', 'Windows' or 'MacOS' expected")
  36. _print_info(f"Platform: {_OS_NAME}")
  37. exit()
  38. if platform == 'LINUX':
  39. # It appears those under LINUX, either don't work or work but play with mouse position
  40. from Xlib.display import Display as _Display
  41. _display = _Display(_env['DISPLAY'])
  42. from Xlib.X import ButtonPress as _ButtonPress, ButtonRelease as _ButtonRelease
  43. from Xlib.ext.xtest import fake_input as _fake_input
  44. from pyautogui import LEFT as _LEFT, RIGHT as _RIGHT, MIDDLE as _MIDDLE
  45. _BUTTON_MAP: _Dict[str, int] = {_LEFT: 1, _MIDDLE:3, _RIGHT: 3}
  46. def _mouse_down(button: str=_LEFT):
  47. assert button in _BUTTON_MAP.keys(), "button argument not in ('left', 'middle', 'right')"
  48. btn = _BUTTON_MAP[button]
  49. _fake_input(_display, _ButtonPress, btn)
  50. _display.sync()
  51. def _mouse_up(button: str=_LEFT):
  52. assert button in _BUTTON_MAP.keys(), "button argument not in ('left', 'middle', 'right')"
  53. btn = _BUTTON_MAP[button]
  54. _fake_input(_display, _ButtonRelease, btn)
  55. _display.sync()
  56. else:
  57. # We can simply use the mouse module under WINDOWS and MACOS
  58. from mouse import press as _mouse_down, release as _mouse_up
  59. from toml import load as _toml_load, TomlDecodeError as _TomlDecodeError
  60. from asyncio import run as _run, sleep as _asleep, TaskGroup as _TaskGroup
  61. def _profile_pre_check(profile_name: str):
  62. if profile_name.endswith(".toml"):
  63. profile_name = profile_name.removesuffix(".toml")
  64. if not _exists(profile_name):
  65. _mkdir(profile_name)
  66. if "SUDO_USER" in _env:
  67. _chown(profile_name, int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  68. if not _exists(_join(profile_name, "keys")):
  69. _mkdir(_join(profile_name, "keys"))
  70. if "SUDO_USER" in _env:
  71. _chown(_join(profile_name, "keys"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  72. def _write_profile(profile_name: str):
  73. if profile_name.endswith(".toml"):
  74. profile_name = profile_name.removesuffix(".toml")
  75. _profile_pre_check(profile_name)
  76. with open(_join(profile_name, profile_name+".toml"), "w") as f:
  77. f.writelines("\n".join([
  78. f'# Tyrell :: v{VERSION} :: More human than human',
  79. '',
  80. '# Placeholders',
  81. '',
  82. "# Enable '{name}', emitting profile name",
  83. "placeholder_name = 'true'",
  84. '',
  85. '# Tick speed',
  86. '# Value in milliseconds',
  87. "# Defaults to 20 ticks per second",
  88. "# Can't be overridden",
  89. 'tick = 50',
  90. '',
  91. '# What is the key to activate/deactivate Tyrell',
  92. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  93. "# Can't be overridden",
  94. "activator = '`'",
  95. '',
  96. '# What is the key to display help',
  97. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  98. "# Defaults to '?'",
  99. "# Can't be overridden",
  100. "helper = 'shift+/'",
  101. '',
  102. '# These can be overridden per key-bind',
  103. '',
  104. '# Delay between each keys',
  105. '# Value in milliseconds',
  106. 'delay = 50',
  107. '',
  108. '# Hold delay for each key',
  109. '# Keys are sent like below:',
  110. '# 1. key down',
  111. '# 2. hold delay',
  112. '# 3. key up',
  113. '# 4. hold delay',
  114. '# 5. delay (see above)',
  115. '# 6. Repeat for keys',
  116. '# Value in milliseconds',
  117. 'hold = 20',
  118. ]))
  119. if "SUDO_USER" in _env:
  120. _chown(_join(profile_name, profile_name+".toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  121. def _write_example(profile_name: str):
  122. if profile_name.endswith(".toml"):
  123. profile_name = profile_name.removesuffix(".toml")
  124. _profile_pre_check(profile_name)
  125. with open(_join(profile_name, "keys", "example.toml"), "w") as f:
  126. f.writelines("\n".join([
  127. '# Example Key-bind',
  128. '',
  129. "# When 'h' is pressed, write 'Hello World!'",
  130. '',
  131. '# Do we want this key-bind active on start-up',
  132. "# This is a simple example, it won't use this",
  133. '#startup = true',
  134. '',
  135. '# Key to trigger on',
  136. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  137. "key = 'h'",
  138. '',
  139. '# Kind of action to perform',
  140. '# click -> Click a mouse button',
  141. '# click down -> Click a mouse button down, but never release up',
  142. '# key -> Type the following keys, pressing each key',
  143. '# key down -> Press the following keys down, never releasing them',
  144. '# mirror -> When the mirror key is pressed down so is key or button, on release so are key or button',
  145. '# See also example_mirror.toml',
  146. "kind = 'key'",
  147. '',
  148. '# Type this',
  149. "# This is required for 'key' and 'key down' kinds (Optional for 'mirror')",
  150. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  151. "# This example will write 'Hello World!'",
  152. "write = 'shift+h, e, l, l, o, space, shift+w, o, r, l, d, shift+1'",
  153. '',
  154. '# If this should repeat X number of ticks',
  155. "# This is a simple example, it won't use this",
  156. '# Assuming tick speed is 20ms, this would occur every second',
  157. '#duration = 50',
  158. '',
  159. '# Use this mouse button',
  160. "# This is a simple example, it won't use this",
  161. "# This is required for 'click' and 'click down' kinds (Optional for 'mirror')",
  162. "# There currently is 'left' or 'right'",
  163. "#button = 'left'",
  164. '',
  165. '# Mirror key',
  166. "# This is a simple example, it won't use this",
  167. '# This is the key whose state will be mirrored',
  168. '# See also example_mirror.toml',
  169. '#',
  170. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  171. "# This example will press 'g' when 'h' is down, and release 'g' when 'h' is up",
  172. "#mirror = 'g'",
  173. '',
  174. '# Optional Overrides',
  175. '',
  176. '# Override delay between each keys',
  177. '# Value in milliseconds',
  178. 'delay = 50',
  179. '',
  180. '# Override hold delay for each key',
  181. '# Keys are sent like below:',
  182. '# 1. key down',
  183. '# 2. hold delay',
  184. '# 3. key up',
  185. '# 4. hold delay',
  186. '# 5. delay (see above)',
  187. '# 6. Repeat for keys',
  188. '# Value in milliseconds',
  189. 'hold = 20',
  190. ]))
  191. if "SUDO_USER" in _env:
  192. _chown(_join(profile_name, "keys", "example.toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  193. def _write_example_mirror(profile_name: str):
  194. if profile_name.endswith(".toml"):
  195. profile_name = profile_name.removesuffix(".toml")
  196. _profile_pre_check(profile_name)
  197. with open(_join(profile_name, "keys", "example_mirror.toml"), "w") as f:
  198. f.writelines("\n".join([
  199. '# Example Mirror Key-bind',
  200. '',
  201. "# Press alt when 'w' is down, and release when 'w' is up",
  202. "# Use 'z' to toggle on/off",
  203. '',
  204. '# Do we want this key-bind active on start-up',
  205. "# This is a simple example, it won't use this",
  206. '#startup = true',
  207. '',
  208. '# Key to toggle',
  209. "key = 'z'",
  210. '',
  211. '# Kind of event',
  212. "kind = 'mirror'",
  213. '',
  214. '# Mirror what key',
  215. "mirror = 'w'",
  216. '',
  217. '# When mirror is down, so will this',
  218. '# When mirror is up, so will this',
  219. "write = 'alt'",
  220. '',
  221. '# Or a mouse button could be configured',
  222. "# But this example doesn't",
  223. "#button = 'right'",
  224. '',
  225. '# You could override delay and hold',
  226. '# Tyrell will turn them off for Mirror kinds automatically',
  227. ]))
  228. if "SUDO_USER" in _env:
  229. _chown(_join(profile_name, "keys", "example_mirror.toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  230. def _match_key(key: str, event: _KeyboardEvent) -> bool:
  231. # Process modifiers
  232. key_mods: _List[str] = []
  233. k = _copy(key)
  234. if 'shift' in k:
  235. k = k.replace('shift+', '')
  236. key_mods.append('shift')
  237. elif 'ctrl' in k:
  238. k = k.replace('ctrl+', '')
  239. key_mods.append('ctrl')
  240. elif 'alt' in k:
  241. k = k.replace('alt+', '')
  242. key_mods.append('alt')
  243. # Check Modifiers
  244. if len(key_mods) == 0 and event.modifiers is not None and len(event.modifiers) != 0:
  245. # We register no modifiers
  246. # Given modifiers (not the same)
  247. return False
  248. if len(key_mods) != 0:
  249. mods_ok: _Dict[str, bool] = {}
  250. for mod in key_mods:
  251. mods_ok[mod] = False
  252. if len(key_mods) != 0 and event.modifiers is None:
  253. # We register modifiers
  254. # Given no modifiers (not the same)
  255. return False
  256. for mod in key_mods:
  257. for m in event.modifiers:
  258. if mod == m:
  259. mods_ok[mod] = True
  260. for mod in mods_ok:
  261. val = mods_ok[mod]
  262. if val is False:
  263. return False
  264. if len(k) == 0:
  265. # It was only a modifier
  266. return True
  267. return event.name == k
  268. def _key_write(write: str, delay:float=50.0, hold:float=20.0):
  269. if ', ' in write:
  270. for part in write.split(', '):
  271. if '+' in part:
  272. mod, key = part.split('+', maxsplit=1)
  273. _key_down(mod)
  274. _key_down(key)
  275. _sleep(hold / 1000)
  276. _key_up(key)
  277. _key_up(mod)
  278. else:
  279. _key_down(part)
  280. _sleep(hold / 1000)
  281. _key_up(part)
  282. _sleep(delay / 1000)
  283. else:
  284. if '+' in write:
  285. mod, key = write.split('+', maxsplit=1)
  286. _key_down(mod)
  287. _key_down(key)
  288. _sleep(hold / 1000)
  289. _key_up(key)
  290. _key_up(mod)
  291. else:
  292. _key_down(write)
  293. _sleep(hold / 1000)
  294. _key_up(write)
  295. _sleep(delay / 1000)
  296. class Action:
  297. # Toggle to determine if this action is 'on' or 'off'
  298. state: bool
  299. # Key to trigger on (toggle on mirror)
  300. key: str
  301. # Kind of action to perform ('click', 'click down', 'key', 'key down', 'mirror')
  302. kind: str
  303. # Optional, keys to send
  304. write: _Option[str]
  305. # Optional, Repeat X number of ticks
  306. duration: _Option[int]
  307. # Internal record of where the duration is
  308. elapsed: _Option[int]
  309. # Optional, mouse button to send
  310. button: _Option[str]
  311. # Optional, Key to mirror
  312. mirror: _Option[str]
  313. # Optional, Override, Delay between each key (in milliseconds)
  314. delay: _Option[int]
  315. # Optional, Override, Hold delay for each key (in milliseconds)
  316. hold: _Option[int]
  317. def __init__(self, data: _Dict[str, _Option[_Any]]):
  318. # Validate data!
  319. assert 'key' in data, "Action missing 'key'"
  320. assert 'kind' in data, "Action missing 'kind'"
  321. assert data['kind'] in ('click', 'click down', 'key', 'key down', 'mirror'), f"Action 'kind' is unsupported, got '{data['kind']}'"
  322. assert 'write' in data or 'button' in data, "Action missing 'write' or 'button' (one needed)"
  323. # Assign class data
  324. if 'startup' in data:
  325. self.state = True
  326. else:
  327. self.state = False
  328. self.key = str(data['key'])
  329. self.kind = str(data['kind'])
  330. if 'write' in data:
  331. self.write = str(data['write'])
  332. else:
  333. self.write = None
  334. if 'button' in data:
  335. self.button = str(data['button'])
  336. else:
  337. self.button = None
  338. if 'mirror' in data:
  339. self.mirror = str(data['mirror'])
  340. else:
  341. self.mirror = None
  342. if 'duration' in data and data['duration'] is not None:
  343. self.duration = int(data['duration'])
  344. self.elapsed = None
  345. else:
  346. self.duration = None
  347. self.elapsed = None
  348. if 'delay' in data and data['delay'] is not None:
  349. self.delay = int(data['delay'])
  350. else:
  351. self.delay = None
  352. if 'hold' in data and data['hold'] is not None:
  353. self.hold = int(data['hold'])
  354. else:
  355. self.hold = None
  356. def is_on(self) -> bool:
  357. return self.state
  358. def toggle(self, on: bool = False, off: bool = False) -> bool:
  359. if (not on and not off) or (on and off):
  360. self.state = not self.state
  361. elif on and not off:
  362. self.state = True
  363. elif off and not on:
  364. self.state = False
  365. if self.state is False and self.is_ticker():
  366. # Reset elapsed time when set to off
  367. if self.elapsed is not None:
  368. self.elapsed = None
  369. elif self.state is True and self.is_ticker():
  370. # Start elapsed time when set to on
  371. if self.elapsed is None:
  372. self.elapsed = self.duration
  373. return self.state
  374. def is_ticker(self) -> bool:
  375. return self.duration is not None
  376. def tick(self) -> bool:
  377. if not self.is_ticker():
  378. return False
  379. if self.elapsed is None:
  380. return False
  381. self.elapsed -= 1
  382. if self.elapsed <= 0:
  383. self.elapsed = self.duration
  384. return True
  385. return False
  386. def trigger_key(self, event: _KeyboardEvent) -> bool:
  387. return _match_key(self.key, event)
  388. def trigger_mirror(self, event: _KeyboardEvent) -> bool:
  389. if self.kind != 'mirror' or self.mirror is None:
  390. return False
  391. return _match_key(self.key, event)
  392. def do(self, global_delay: int=50, global_hold: int=20, mirror: str=""):
  393. if self.kind == 'key down' and self.write is not None:
  394. _key_down(self.write)
  395. elif self.kind == 'click down' and self.button is not None:
  396. _mouse_down(self.button)
  397. elif self.kind == 'key' and self.write is not None:
  398. if self.hold is not None:
  399. if self.delay is not None:
  400. _key_write(self.write, delay=self.delay, hold=self.hold)
  401. else:
  402. _key_write(self.write, delay=global_delay, hold=self.hold)
  403. else:
  404. if self.delay is not None:
  405. _key_write(self.write, delay=self.delay, hold=global_hold)
  406. else:
  407. _key_write(self.write, delay=global_delay, hold=global_hold)
  408. elif self.kind == 'click' and self.button is not None:
  409. _mouse_down(self.button)
  410. if self.hold is not None:
  411. _sleep(self.hold / 1000)
  412. else:
  413. _sleep(global_hold / 1000)
  414. _mouse_up(self.button)
  415. if self.delay is not None:
  416. _sleep(self.delay / 1000)
  417. else:
  418. _sleep(global_delay / 1000)
  419. elif self.kind == 'mirror':
  420. if self.write is not None:
  421. if mirror == 'down':
  422. _key_down(self.write)
  423. elif mirror == 'up':
  424. _key_up(self.write)
  425. elif self.button is not None:
  426. if mirror == 'down':
  427. _mouse_down(self.button)
  428. elif mirror == 'up':
  429. _mouse_up(self.button)
  430. class Profile:
  431. name: str
  432. placeholders: _Dict[str, bool]
  433. tick: int
  434. delay: int
  435. hold: int
  436. activator: str
  437. helper: str
  438. _actions: _Dict[str, Action]
  439. _state: bool
  440. _help_screen: _List[str]
  441. def __init__(self, name: str):
  442. self._state = False
  443. # Unify name without extension
  444. if name.endswith(".toml"):
  445. self.name = name.removesuffix(".toml")
  446. else:
  447. self.name = name
  448. # Pre-Check
  449. if not _exists(self.name):
  450. # Does not exist, new everything
  451. _write_profile(self.name)
  452. _write_example(self.name)
  453. _write_example_mirror(self.name)
  454. else:
  455. # Profile config?
  456. if not _exists(_join(self.name, self.name+".toml")):
  457. _write_profile(self.name)
  458. # Do we have keys?
  459. if not _exists(_join(self.name, "keys")):
  460. _write_example(self.name)
  461. _write_example_mirror(self.name)
  462. if self.name == "_example":
  463. _print_warn("Got example profile, creation only")
  464. return # Done with example code
  465. # Load toml config
  466. with open(_join(self.name, self.name+".toml"), "r") as f:
  467. dat = _toml_load(f)
  468. self.placeholders = {
  469. "name": False,
  470. }
  471. self.tick = 50
  472. self.activator = "`"
  473. self.helper = "shift+/" # ?
  474. self.delay = 50
  475. self.hold = 20
  476. for key in dat:
  477. val = dat[key]
  478. if key.startswith("placeholder_"):
  479. if str(val) in ('true', 'TRUE', 'True', 'T', 't', 'yes', 'YES', 'Yes', 'Y', 'y', '1'):
  480. self.placeholders[key.removeprefix("placeholder_")] = True
  481. else:
  482. self.placeholders[key.removeprefix("placeholder_")] = False
  483. elif key == "tick":
  484. self.tick = int(val)
  485. elif key == "delay":
  486. self.delay = int(val)
  487. elif key == "hold":
  488. self.hold = int(val)
  489. elif key == "activator":
  490. self.activator = str(val)
  491. elif key == "helper":
  492. self.helper = str(val)
  493. # Locate and "load" actions
  494. self._actions = {}
  495. self._help_screen = [
  496. _style(f"{self.activator:15}", fg='bright_magenta') + " Activator",
  497. _style(f"{self.helper:15}", fg='bright_magenta') + " Help",
  498. _style(f"{'ctrl+c':15}", fg='bright_red') + " Quit",
  499. "",
  500. ]
  501. for entry in _listdir(_join(self.name, "keys")):
  502. if entry.startswith(".") or _isdir(entry) or not entry.endswith(".toml"):
  503. _print_info(f"{_join(self.name, "keys", entry)} => skipped")
  504. continue
  505. # Must be a toml file, probably an action
  506. file = _join(self.name, "keys", entry)
  507. try:
  508. data = _toml_load(file)
  509. a = Action(data)
  510. if a.kind != 'mirror':
  511. if a.duration is not None:
  512. if a.duration > 0:
  513. d = int(a.duration*self.tick)
  514. if d < 1000:
  515. self._help_screen.append(_style(f"{a.key:15}", fg='bright_cyan') + f" executes {entry.removesuffix(".toml")} every {a.duration} ticks ({d}ms)")
  516. else:
  517. df = float(d / 1000.0)
  518. self._help_screen.append(_style(f"{a.key:15}", fg='bright_cyan') + f" executes {entry.removesuffix(".toml")} every {a.duration} ticks ({df:0.3}s)")
  519. else:
  520. self._help_screen.append(_style(f"{a.key:15}", fg='bright_cyan') + f" executes {entry.removesuffix(".toml")} every tick ({int(self.tick)}ms)")
  521. else:
  522. self._help_screen.append(_style(f"{a.key:15}", fg='bright_cyan') + f" executes {entry.removesuffix(".toml")}")
  523. else:
  524. self._help_screen.append(_style(f"{a.key:15}", fg='bright_cyan') + f" toggles {entry.removesuffix(".toml")} (Mirrors {_style(f'{a.mirror}', fg='bright_cyan')})")
  525. if a.write is not None:
  526. a.write = self.fill_in(a.write)
  527. self._actions[entry.removesuffix(".toml")] = a
  528. except _TomlDecodeError:
  529. _print_warn(f"{file} => invalid toml")
  530. def fill_in(self, text: str) -> str:
  531. if self.placeholders['name']:
  532. if "{name}" in text:
  533. return text.replace("{name}", self.name)
  534. return text
  535. def is_on(self) -> bool:
  536. return self._state
  537. def toggle(self, on: bool=False, off: bool=False) -> bool:
  538. if (not on and not off) or (on and off):
  539. self._state = not self._state
  540. elif on and not off:
  541. self._state = True
  542. elif off and not on:
  543. self._state = False
  544. return self._state
  545. def show_help(self):
  546. _echo(_style("Tyr", fg='bright_red') + _style("ell", fg='bright_green') + " v" + _style(f"{VERSION}", fg='bright_cyan'))
  547. _echo("Profile: " + _style(f"{self.name}", fg='bright_yellow'))
  548. for line in self._help_screen:
  549. _echo(line)
  550. def _trigger_activator(self, event: _KeyboardEvent) -> bool:
  551. return _match_key(self.activator, event)
  552. def _trigger_helper(self, event: _KeyboardEvent) -> bool:
  553. return _match_key(self.helper, event)
  554. def _callback(self, event: _KeyboardEvent):
  555. # Quickly process CTRL+C as quit
  556. if event.modifiers is not None:
  557. if 'ctrl' in event.modifiers:
  558. if event.name == 'c':
  559. return
  560. # Process activator and helper (These occur before actions defined)
  561. if event.event_type == 'up':
  562. if self._trigger_activator(event):
  563. if self.toggle():
  564. _print_on("Tyrell")
  565. else:
  566. _print_off("Tyrell")
  567. return
  568. elif self._trigger_helper(event) and self.is_on():
  569. _print_ok("Help")
  570. self.show_help()
  571. self.toggle(off=True)
  572. _print_off("Tyrell")
  573. return
  574. # Process actions defined
  575. for act in self._actions:
  576. a = self._actions[act]
  577. if a.trigger_key(event) and event.event_type == 'up' and self.is_on():
  578. if a.kind != 'mirror' and not a.is_ticker():
  579. #_print_ok(f"{act} => '{a.kind}' action, trigger on '{a.key}'")
  580. _print_ok(f"{act}")
  581. a.do(self.delay, self.hold)
  582. elif a.kind == 'mirror' or a.is_ticker():
  583. if a.toggle():
  584. _print_on(f"{act}")
  585. else:
  586. _print_off(f"{act}")
  587. #_print_ok(f"{act} => '{a.kind}' action, trigger on '{a.mirror}' (toggle with '{a.key}')")
  588. self.toggle(off=True)
  589. _print_off("Tyrell")
  590. return
  591. elif a.trigger_mirror(event):
  592. if a.is_on() and event.event_type is not None:
  593. #_print_warn(f"{act} => mirror '{a.mirror}' is {event.event_type}")
  594. a.do(self.delay, self.hold, mirror=event.event_type)
  595. async def _ticker(self):
  596. while True:
  597. for act in self._actions:
  598. a = self._actions[act]
  599. if a.tick():
  600. #_print_warn(f"{act} => tick event fired")
  601. a.do(self.delay, self.hold)
  602. await _asleep(self.tick / 1000)
  603. async def run(self):
  604. self.show_help()
  605. _h = _key_hook(self._callback)
  606. # I'm not sure why if I don't have any async TaskGroup
  607. # We get some ugly errors
  608. """
  609. Task was destroyed but it is pending!
  610. task: <Task pending name='Task-3' coro=<BaseEventLoop.shutdown_default_executor() running at /usr/lib/python3.12/asyncio/base_events.py:586>>
  611. tyrell.py:607: RuntimeWarning: coroutine 'BaseEventLoop.shutdown_default_executor' was never awaited
  612. RuntimeWarning: Enable tracemalloc to get the object allocation traceback
  613. OR
  614. tyrell.py:571: RuntimeWarning: coroutine 'BaseEventLoop.shutdown_default_executor' was never awaited
  615. RuntimeWarning: Enable tracemalloc to get the object allocation traceback
  616. """
  617. async with _TaskGroup() as background:
  618. background.create_task(self._ticker())
  619. _key_unhook(_h)
  620. @_command()
  621. @_arg('profile', required=True)
  622. def _main(profile: str):
  623. p = Profile(profile)
  624. try:
  625. _run(p.run())
  626. except KeyboardInterrupt:
  627. print()
  628. if __name__ == "__main__":
  629. if not _exists("_example"):
  630. _print_warn("Missing '_example' profile, creating...")
  631. Profile("_example")
  632. _print_ok("")
  633. _main()