Skip to content

Instantly share code, notes, and snippets.

@elpekenin
Last active December 4, 2022 14:04
Show Gist options
  • Save elpekenin/a83603d35a99e05698f9f899ff96afb0 to your computer and use it in GitHub Desktop.
Save elpekenin/a83603d35a99e05698f9f899ff96afb0 to your computer and use it in GitHub Desktop.
#!/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