tyrell.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. from sys import argv as _argv
  2. from os import name as _OS_NAME, environ as _env
  3. from os import listdir as _listdir, mkdir as _mkdir, chown as _chown
  4. from os.path import exists as _exists, join as _join, isdir as _isdir
  5. from time import sleep as _sleep
  6. from typing import Dict as _Dict, Tuple as _Tuple, List as _List, Optional as _Option, Any as _Any
  7. try:
  8. from click import echo as _echo, style as _style
  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_info(msg: str):
  19. print(" " + msg)
  20. from pyautogui import LEFT as _LEFT, RIGHT as _RIGHT, MIDDLE as _MIDDLE
  21. from keyboard import press as _key_down, release as _key_up, KeyboardEvent as _KeyboardEvent, add_hotkey as _add_hotkey, hook as _key_hook, write as _key_write
  22. _BUTTON_MAP: _Dict[str | int, int] = {_LEFT: 1, _MIDDLE:3, _RIGHT: 3, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7}
  23. platform: str = ""
  24. os_name = _OS_NAME.lower()
  25. if "linux" in os_name or "posix" in os_name:
  26. platform = "LINUX"
  27. elif "windows" in os_name or "nt" in os_name:
  28. platform = "WINDOWS"
  29. elif "darwin" in os_name:
  30. platform = "MACOS"
  31. else:
  32. _print_err("Platform 'Linux', 'Windows' or 'MacOS' expected")
  33. _print_info(f"Platform: {_OS_NAME}")
  34. exit()
  35. if platform == 'LINUX':
  36. # It appears those under LINUX, either don't work or work but play with mouse position
  37. from Xlib.display import Display as _Display
  38. _display = _Display(_env['DISPLAY'])
  39. from Xlib.X import ButtonPress as _ButtonPress, ButtonRelease as _ButtonRelease
  40. from Xlib.ext.xtest import fake_input as _fake_input
  41. def _mouse_down(button: str | int=_LEFT):
  42. assert button in _BUTTON_MAP.keys(), "button argument not in ('left', 'middle', 'right', 1, 2, 3, 4, 5, 6, 7)"
  43. btn = _BUTTON_MAP[button]
  44. _fake_input(_display, _ButtonPress, btn)
  45. _display.sync()
  46. def _mouse_up(button: str | int=_LEFT):
  47. assert button in _BUTTON_MAP.keys(), "button argument not in ('left', 'middle', 'right', 1, 2, 3, 4, 5, 6, 7)"
  48. btn = _BUTTON_MAP[button]
  49. _fake_input(_display, _ButtonRelease, btn)
  50. _display.sync()
  51. def _click(button: str|int=_LEFT):
  52. assert button in _BUTTON_MAP.keys(), "button argument not in ('left', 'middle', 'right', 1, 2, 3, 4, 5, 6, 7)"
  53. _mouse_down(button)
  54. _mouse_up(button)
  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, click as _click
  58. from toml import load as _toml_load
  59. from asyncio import run as _run, sleep as _asleep
  60. from asyncio.tasks import gather as _task_group
  61. def _keyboard_output(msg: str, delay: int=50, hold: int=20):
  62. _key_write(msg, delay=hold)
  63. _sleep(delay/1000)
  64. def _mouse_output(button: str, delay: int=50, hold: int=20):
  65. _mouse_down(button=button)
  66. _sleep(hold / 1000)
  67. _mouse_up(button=button)
  68. _sleep(hold / 1000)
  69. _sleep(delay / 1000)
  70. def _profile_pre_check(profile_name: str):
  71. if profile_name.endswith(".toml"):
  72. profile_name = profile_name.removesuffix(".toml")
  73. if not _exists(profile_name):
  74. _mkdir(profile_name)
  75. if "SUDO_USER" in _env:
  76. _chown(profile_name, int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  77. if not _exists(_join(profile_name, "keys")):
  78. _mkdir(_join(profile_name, "keys"))
  79. if "SUDO_USER" in _env:
  80. _chown(_join(profile_name, "keys"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  81. def _write_profile(profile_name: str):
  82. if profile_name.endswith(".toml"):
  83. profile_name = profile_name.removesuffix(".toml")
  84. _profile_pre_check(profile_name)
  85. with open(_join(profile_name, profile_name+".toml"), "w") as f:
  86. f.writelines("\n".join([
  87. '# Tyrell :: v0.1-dev :: More human than human',
  88. '',
  89. '# Placeholders',
  90. '',
  91. "# Enable '{name}', emitting profile name",
  92. "placeholder_name = 'true'",
  93. '',
  94. '# Tick speed',
  95. '# Value in milliseconds',
  96. "# Defaults to 20 ticks per second",
  97. "# Can't be overridden",
  98. 'tick = 50',
  99. '',
  100. '# What is the key to activate/deactivate Tyrell',
  101. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  102. "# Can't be overridden",
  103. "activator = '`'",
  104. '',
  105. '# What is the key to display help',
  106. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  107. "# Defaults to '?'",
  108. "# Can't be overridden",
  109. "helper = 'shift+/'",
  110. '',
  111. '# These can be overridden per key-bind',
  112. '',
  113. '# Delay between each keys',
  114. '# Value in milliseconds',
  115. 'delay = 50',
  116. '',
  117. '# Hold delay for each key',
  118. '# Keys are sent like below:',
  119. '# 1. key down',
  120. '# 2. hold delay',
  121. '# 3. key up',
  122. '# 4. hold delay',
  123. '# 5. delay (see above)',
  124. '# 6. Repeat for keys',
  125. '# Value in milliseconds',
  126. 'hold = 20',
  127. ]))
  128. if "SUDO_USER" in _env:
  129. _chown(_join(profile_name, profile_name+".toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  130. def _write_example(profile_name: str):
  131. if profile_name.endswith(".toml"):
  132. profile_name = profile_name.removesuffix(".toml")
  133. _profile_pre_check(profile_name)
  134. with open(_join(profile_name, "keys", "example.toml"), "w") as f:
  135. f.writelines("\n".join([
  136. '# Example Key-bind',
  137. '',
  138. "# When 'h' is pressed, write 'Hello World!'",
  139. '',
  140. '# Do we want this key-bind active on start-up',
  141. "# This is a simple example, it won't use this",
  142. '#startup = true',
  143. '',
  144. '# Key to trigger on',
  145. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  146. "key = 'h'",
  147. '',
  148. '# Kind of action to perform',
  149. '# click -> Click a mouse button',
  150. '# click down -> Click a mouse button down, but never release up',
  151. '# key -> Type the following keys, pressing each key',
  152. '# key down -> Press the following keys down, never releasing them',
  153. '# mirror -> When the mirror key is pressed down so is key or button, on release so are key or button',
  154. '# See also example_mirror.toml',
  155. "kind = 'key'",
  156. '',
  157. '# Type this',
  158. "# This is required for 'key' and 'key down' kinds (Optional for 'mirror')",
  159. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  160. "# This example will write 'Hello World!'",
  161. "write = 'shift+h, e, l, l, o, space, shift+w, o, r, l, d, shift+1'",
  162. '',
  163. '# If this should repeat X number of ticks',
  164. "# This is a simple example, it won't use this",
  165. '# Assuming tick speed is 20ms, this would occur every second',
  166. '#duration = 50',
  167. '',
  168. '# Use this mouse button',
  169. "# This is a simple example, it won't use this",
  170. "# This is required for 'click' and 'click down' kinds (Optional for 'mirror')",
  171. "# There currently is 'left' or 'right'",
  172. "#button = 'left'",
  173. '',
  174. '# Mirror key',
  175. "# This is a simple example, it won't use this",
  176. '# This is the key whose state will be mirrored',
  177. '# See also example_mirror.toml',
  178. '#',
  179. "# This hotkey must be in the format, 'ctrl+shift+a' (user holds ctrl, shift and 'a' at once)",
  180. "# This example will press 'g' when 'h' is down, and release 'g' when 'h' is up",
  181. "#mirror = 'g'",
  182. '',
  183. '# Optional Overrides',
  184. '',
  185. '# Override delay between each keys',
  186. '# Value in milliseconds',
  187. 'delay = 50',
  188. '',
  189. '# Override hold delay for each key',
  190. '# Keys are sent like below:',
  191. '# 1. key down',
  192. '# 2. hold delay',
  193. '# 3. key up',
  194. '# 4. hold delay',
  195. '# 5. delay (see above)',
  196. '# 6. Repeat for keys',
  197. '# Value in milliseconds',
  198. 'hold = 20',
  199. ]))
  200. if "SUDO_USER" in _env:
  201. _chown(_join(profile_name, "keys", "example.toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  202. def _write_example_mirror(profile_name: str):
  203. if profile_name.endswith(".toml"):
  204. profile_name = profile_name.removesuffix(".toml")
  205. _profile_pre_check(profile_name)
  206. with open(_join(profile_name, "keys", "example_mirror.toml"), "w") as f:
  207. f.writelines("\n".join([
  208. '# Example Mirror Key-bind',
  209. '',
  210. "# Press alt when 'w' is down, and release when 'w' is up",
  211. "# Use 'z' to toggle on/off",
  212. '',
  213. '# Do we want this key-bind active on start-up',
  214. "# This is a simple example, it won't use this",
  215. '#startup = true',
  216. '',
  217. '# Key to toggle',
  218. "key = 'z'",
  219. '',
  220. '# Kind of event',
  221. "kind = 'mirror'",
  222. '',
  223. '# Mirror what key',
  224. "mirror = 'w'",
  225. '',
  226. '# When mirror is down, so will this',
  227. '# When mirror is up, so will this',
  228. "write = 'alt'",
  229. '',
  230. '# Or a mouse button could be configured',
  231. "# But this example doesn't",
  232. "#button = 'right'",
  233. '',
  234. '# You could override delay and hold',
  235. '# Tyrell will turn them off for Mirror kinds automatically',
  236. ]))
  237. if "SUDO_USER" in _env:
  238. _chown(_join(profile_name, "keys", "example_mirror.toml"), int(_env['SUDO_UID']), int(_env['SUDO_GID']))
  239. class Action:
  240. # Toggle to determine if this action is 'on' or 'off'
  241. state: bool
  242. # Key to trigger on (toggle on mirror)
  243. key: str
  244. # Kind of action to perform ('click', 'click down', 'key', 'key down', 'mirror')
  245. kind: str
  246. # Optional, keys to send
  247. write: _Option[str]
  248. # Optional, Repeat X number of ticks
  249. duration: _Option[int]
  250. # Optional, mouse button to send
  251. button: _Option[str]
  252. # Optional, Key to mirror
  253. mirror: _Option[str]
  254. # Optional, Override, Delay between each key (in milliseconds)
  255. delay: _Option[int]
  256. # Optional, Override, Hold delay for each key (in milliseconds)
  257. hold: _Option[int]
  258. def __init__(self, data: _Dict[str, _Option[_Any]]):
  259. # Validate data!
  260. assert 'key' in data, "Action missing 'key'"
  261. assert 'kind' in data, "Action missing 'kind'"
  262. assert data['kind'] in ('click', 'click down', 'key', 'key down', 'mirror'), f"Action 'kind' is unsupported, got '{data['kind']}'"
  263. assert 'write' in data or 'button' in data, "Action missing 'write' or 'button' (one needed)"
  264. # Assign class data
  265. if 'startup' in data:
  266. self.state = True
  267. else:
  268. self.state = False
  269. self.key = str(data['key'])
  270. self.kind = str(data['kind'])
  271. if 'write' in data:
  272. self.write = str(data['write'])
  273. else:
  274. self.write = None
  275. if 'button' in data:
  276. self.button = str(data['button'])
  277. else:
  278. self.button = None
  279. if 'mirror' in data:
  280. self.mirror = str(data['mirror'])
  281. else:
  282. self.mirror = None
  283. if 'duration' in data:
  284. self.duration = int(data['duration'])
  285. else:
  286. self.duration = None
  287. if 'delay' in data:
  288. self.delay = int(data['delay'])
  289. else:
  290. self.delay = None
  291. if 'hold' in data:
  292. self.hold = int(data['hold'])
  293. else:
  294. self.hold = None
  295. class Profile:
  296. name: str
  297. placeholders: _Dict[str, bool]
  298. tick: int
  299. delay: int
  300. hold: int
  301. activator: str
  302. helper: str
  303. def __init__(self, name: str):
  304. # Unify name without extension
  305. if name.endswith(".toml"):
  306. self.name = name.removesuffix(".toml")
  307. else:
  308. self.name = name
  309. # Pre-Check
  310. if not _exists(self.name):
  311. # Does not exist, new everything
  312. _write_profile(self.name)
  313. _write_example(self.name)
  314. _write_example_mirror(self.name)
  315. else:
  316. # Profile config?
  317. if not _exists(_join(self.name, self.name+".toml")):
  318. _write_profile(self.name)
  319. # Do we have keys?
  320. if not _exists(_join(self.name, "keys")):
  321. _write_example(self.name)
  322. _write_example_mirror(self.name)
  323. # Initial load
  324. self.reload()
  325. def reload(self):
  326. # Load toml config
  327. with open(_join(self.name, self.name+".toml"), "r") as f:
  328. dat = _toml_load(f)
  329. self.placeholders = {
  330. "name": False,
  331. }
  332. self.tick = 50
  333. self.activator = "`"
  334. self.helper = "shift+/"
  335. self.delay = 50
  336. self.hold = 20
  337. for key in dat:
  338. val = dat[key]
  339. if key.startswith("placeholder_"):
  340. if str(val) in ('true', 'TRUE', 'True', 'T', 't', 'yes', 'YES', 'Yes', 'Y', 'y', '1'):
  341. self.placeholders[key.removeprefix("placeholder_")] = True
  342. else:
  343. self.placeholders[key.removeprefix("placeholder_")] = False
  344. elif key == "tick":
  345. self.tick = int(val)
  346. elif key == "delay":
  347. self.delay = int(val)
  348. elif key == "hold":
  349. self.hold = int(val)
  350. elif key == "activator":
  351. self.activator = str(val)
  352. elif key == "helper":
  353. self.helper = str(val)
  354. if __name__ == "__main__":
  355. if not _exists("_example"):
  356. _print_warn("Missing '_example' profile, creating...")
  357. p = Profile("_example")
  358. _print_ok("")
  359. exit()
  360. _print_ok("All okay")