Created
May 2, 2024 12:41
Run a CMD whenever particular words are typed in any window
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Run `CMD` whenever any of the `FORBIDDEN_WORDS` is typed. | |
Reading keyboard input regardless of window focus requires sudo: | |
```sh | |
python3 -m venv venv | |
venv/bin/pip install keyboard==0.13.5 | |
sudo venv/bin/python main.py | |
``` | |
""" | |
import os | |
import keyboard | |
EOT = chr(4) | |
NULL = chr(0) | |
# NOTE: capitalization is NOT taken into account and only | |
# ASCII letters are supported. | |
FORBIDDEN_WORDS = [ | |
"hello", | |
"world", | |
] | |
USER = "<insert user>" | |
CMD = f""" | |
sudo -H -u {USER} bash -c 'xdg-open https://www.youtube.com/watch?v=dQw4w9WgXcQ' | |
""" | |
class CircularBuffer: | |
def __init__(self): | |
self.n = 256 | |
# Otherwise the longest word can never be matched. | |
assert self.n >= max(len(word) for word in FORBIDDEN_WORDS) | |
self.buffer = self.n * [EOT] | |
self.tail = 0 | |
def add(self, c): | |
self.buffer[self.tail] = c | |
self.tail = (self.tail + 1) % self.n | |
return None | |
def __iter__(self): | |
p = (self.tail - 1) % self.n | |
cnt = 0 | |
while self.buffer[p] != EOT and cnt < self.n: | |
yield self.buffer[p] | |
p = (p - 1) % self.n | |
cnt += 1 | |
class Trie: | |
class _Node: | |
def __init__(self, val): | |
assert len(val) == 1 | |
self.val = val | |
self.children: dict[str, Trie._Node] = {} | |
def __init__(self, words=None): | |
self.root = Trie._Node(NULL) | |
if words is not None: | |
self._build(words) | |
def _build(self, words): | |
for word in words: | |
curr = self.root | |
for char in word: | |
if (nxt := curr.children.get(char)) is None: | |
node = Trie._Node(char) | |
curr.children[char] = node | |
nxt = node | |
curr = nxt | |
# Signify the end of a word. | |
curr.children[EOT] = Trie._Node(NULL) | |
def match(self, it) -> bool: | |
curr = self.root | |
for char in it: | |
if (nxt := curr.children.get(char)) is None: | |
return False | |
if EOT in nxt.children: | |
return True | |
curr = nxt | |
return False | |
def main(): | |
buffer = CircularBuffer() | |
# Reverse words st. we can match based on changes | |
# at the tail. | |
fwords = ["".join(reversed(word)) for word in FORBIDDEN_WORDS] | |
trie = Trie(fwords) | |
while True: | |
event = keyboard.read_event() | |
if event.event_type != "down": | |
continue | |
elif keyboard.is_modifier(event.name): | |
continue | |
char = event.name or NULL | |
# E.g. the `keyboard` module returns "space" instead of " " | |
# I don't want to conform exactly to the module as I would | |
# have to make changes if the module does. So everything | |
# not 1 ASCII character will be assigned the NULL char. | |
# So I still note the press, just not what it was. | |
if len(char) > 1 or not char.isascii(): | |
char = NULL | |
buffer.add(char) | |
if trie.match(buffer): | |
os.system(CMD) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment