Last active
December 4, 2022 14:04
-
-
Save elpekenin/a83603d35a99e05698f9f899ff96afb0 to your computer and use it in GitHub Desktop.
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
#!/usr/bin/env python3 | |
# ======================== DISCLAIMER ======================== | |
# Haven't tested this at all, so you may need some adjustments | |
# Firmware-side you'd need: | |
# a) Enable RAW_HID: | |
# https://docs.qmk.fm/#/feature_rawhid?id=raw-hid | |
# | |
# b) Check layer-control functions here: | |
# https://docs.qmk.fm/#/feature_layers?id=functions | |
# | |
# c) Add something similar to this: | |
# void raw_hid_receive(uint8_t *data, uint8_t length) { | |
# layer_move(data[0]); | |
# //Answering the message might be needed | |
# } | |
# | |
# TODO: Run in background on Windows | |
# Linux(and probably Mac)should work if using the `&` operator | |
# ============================================================ | |
import logging | |
# ============ YOUR CONFIG HERE ============ | |
# Time in seconds to wait between checks | |
CHECK_RATE = 0.2 | |
# Your program->layer mapping | |
LAYERS = { | |
"program_name": 0, | |
} | |
# Logging level, useful for debugging | |
# CRITICAL => most stuff is silenced | |
LOG_LEVEL = logging.DEBUG | |
# Change to `True` to see program names | |
SHOW_NAMES = True | |
# Change if using XAP or custom RAW endpoint | |
USAGE_PAGE = 0xFF60 | |
USAGE = 0x0061 | |
# ========================================== | |
## Don't touch any code down here unless you know what you're doing | |
# Globally available dict where some info gets stored | |
INFO = {} | |
logging.basicConfig( | |
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
level=LOG_LEVEL, | |
) | |
def check_dependencies(): | |
"""This function will check for the needed dependencies based on the OS. | |
If something is not installed, user will be prompted to install it automatically. | |
""" | |
import importlib | |
import subprocess | |
import sys | |
COMMON_DEPENDENCIES = ["hid"] | |
DEPENDENCIES = { | |
"linux": ["Xlib", *COMMON_DEPENDENCIES], | |
"win32": ["win32gui", *COMMON_DEPENDENCIES], | |
} | |
platform = sys.platform | |
deps = DEPENDENCIES.get(platform) | |
if deps is None: | |
logging.error(f"Detected {platform} which isn't supported/tested") | |
sys.exit(1) | |
logging.info(f"Detected {platform}, checking dependencies...") | |
INFO["platform"] = platform | |
for dep in deps: | |
try: | |
importlib.import_module(dep) | |
logging.info(f"{dep} was already installed ✔") | |
except Exception as e: | |
logging.error(f"Error while trying to import {dep}: [{e.__class__.__name__}]{e}") | |
ans = input(f"Module {dep} not installed, do you want to install it? [Y/n]: ") | |
if ans.lower() in ["", "y", "yes"]: | |
logging.info(f"Importing {dep} through pip") | |
subprocess.run([sys.executable, "-m", "pip", "install", dep]) | |
print("-------------") | |
def get_device(): | |
devices = [i for i in hid.enumerate() if i["usage_page"] == USAGE_PAGE and i["usage"] == USAGE] | |
if not devices: | |
import sys | |
print("No devices found, quitting") | |
sys.exit(0) | |
if len(devices) == 1: | |
return hid.Device(path=devices[0]["path"]) | |
text = "\n".join([ | |
*[f"{j['manufacturer_string']}, {j['product_string']} [{i}]" for i, j in enumerate(devices)], | |
"Found more than 1 device, please select one [0]: ", | |
]) | |
index = input(text) | |
if not index: | |
index = 0 | |
return hid.Device(path=devices[int(index)]["path"]) | |
def _get_active_window_linux(): | |
from Xlib.display import Display | |
return Display().get_input_focus().focus.get_wm_class()[0] | |
def _get_active_window_windows(): | |
from win32gui import GetWindowText, GetForegroundWindow | |
return GetWindowText(GetForegroundWindow()) | |
def get_active_window(): | |
"""This function retrieves the name of the currently active window, adapted to each OS | |
""" | |
FUNCTION = { | |
"linux": _get_active_window_linux, | |
"win32": _get_active_window_windows, | |
} | |
# At this point the OS should 100% be supported, but adding fallback function just in case | |
return FUNCTION.get(INFO["platform"], lambda: "Your OS isn't supported, how did we end up here?")() | |
if __name__ == "__main__": | |
try: | |
check_dependencies() | |
global hid | |
import hid | |
device = get_device() | |
import time | |
previous = "" | |
# Main loop | |
while True: | |
time.sleep(CHECK_RATE) | |
current = get_active_window() | |
if SHOW_NAMES: | |
print(f"Current window's name is: {current}") | |
if current == previous: | |
logging.info(f"Window is still the same, not doing anything") | |
continue | |
previous = current | |
# Read the layer to which program is mapped | |
layer = LAYERS.get(current) | |
if not layer: | |
logging.info(f"{current} was not found on LAYERS") | |
continue | |
# -- Send message | |
# Create empty packet each time, just in case something goes wrong | |
request_packet = [0x00] * 32 | |
# Set payload | |
request_packet[0] = layer | |
# Windows needs an extra heading 0 | |
if INFO["platform"] == "win32": | |
request_packet = [0x00, *request_packet] | |
device.write(bytes(request_packet)) | |
logging.info(f"Sent: {request_packet}") | |
response_packet = device.read(32, timeout=1000) | |
logging.info(f"Received: {response_packet}") | |
finally: | |
device.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment