Skip to content

Instantly share code, notes, and snippets.

@yannickperrenet
Created May 2, 2024 12:41
Show Gist options
  • Save yannickperrenet/2f6801307199774c6b3e6575e401bf88 to your computer and use it in GitHub Desktop.
Save yannickperrenet/2f6801307199774c6b3e6575e401bf88 to your computer and use it in GitHub Desktop.
Run a CMD whenever particular words are typed in any window
"""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