tyrell.py 26 KB

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