From fcce9177e356bb27283926451433130a8809fcb0 Mon Sep 17 00:00:00 2001 From: Sébastien Dailly Date: Sun, 27 Aug 2023 16:41:10 +0200 Subject: Send the whole keymap to the device --- .gitignore | 1 + macropad.pyw | 33 ++++++++++++----------- readme.rst | 84 +++++++++++++++++++++++++++++++++++++++++++++------------- socket_conn.py | 1 + win32.py | 18 ++++++++----- xlib.py | 44 +++++++++++++++++++++--------- 6 files changed, 129 insertions(+), 52 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/macropad.pyw b/macropad.pyw index f35e141..2687065 100755 --- a/macropad.pyw +++ b/macropad.pyw @@ -25,8 +25,6 @@ config_file = path.join(script_path, "config.ini") config = configparser.ConfigParser(delimiters="=") config.read(config_file) - - init_mapping = config["mapping"] mapping = dict(init_mapping) @@ -43,14 +41,14 @@ from sys import platform if platform == "win32": import win32 window_listener = win32.Listener(mapping, q) - window_listener.start() component.provideUtility(window_listener, interfaces.desktopEvent.IDesktop) + window_listener.start() elif platform == 'linux': import xlib xlib_listener = xlib.Listener(mapping, q) - xlib_listener.start() component.provideUtility(xlib_listener, interfaces.desktopEvent.IDesktop) + xlib_listener.start() # # How to connect to the peripherical @@ -69,16 +67,14 @@ elif config.has_section("connection.socket"): from socket_conn import SocketConnection endpoint = component.queryAdapter(SocketConnection(config["connection.socket"]), endpoint.IEndpoint) endpoint.queue = q - endpoint.connect() component.provideUtility(endpoint, interfaces.endpoint.IEndpoint) + endpoint.connect() if config.has_section("socket.serve"): import socketserver server = socketserver.Handler(config["socket.serve"], q) - else: - server = None @@ -95,7 +91,8 @@ class Icon(object): self.stop = threading.Event() self.show_hide = threading.Event() - # Start the icon into a new thread in order to keep the main loop control + # Start the icon into a new thread in order to keep the main loop + # control icon_thread = threading.Thread(target=self.icon.run) self.icon_thread = icon_thread @@ -192,13 +189,19 @@ class Application(object): return self.last_layout = data - if data == "Windows": - j = [ {"layout": data} ] - else: - # Add the Windows layout in the bottom layer - j = [ {"layout": "Windows"}, {"stack": data} ] - - component.queryUtility(interfaces.endpoint.IEndpoint).send(j) + if isinstance(data, dict): + component.queryUtility(interfaces.endpoint.IEndpoint).send(data) + return + elif isinstance(data, str): + if not path.exists(data): + print("The file '%s' does not exists" % data) + return + with open(data, "r") as json_file: + json_data = json_file.read() + j = json.loads(json_data) + content = json.dumps(j) + + component.queryUtility(interfaces.endpoint.IEndpoint).send(j) def associate(self, layout: str, name: str): mapping[name] = layout diff --git a/readme.rst b/readme.rst index b8ffca2..f285538 100755 --- a/readme.rst +++ b/readme.rst @@ -1,12 +1,32 @@ +.. role:: json(code) + :language: json + +======================= +Smart Macropad with KMK +======================= + +.. contents:: + :depth: 2 + +------------- +The companion +------------- + +The companion is running in the PC connected with the macropad. The application +will control the keyboard and changes the keys depending of the application used. Requirements ============ -python3 -m pip install pystray pyserial zope.component +.. code:: bash + + python3 -m pip install pystray pyserial zope.component for debian you may need to install : -sudo apt install python3-pil.imagetk +.. code:: bash + + sudo apt install python3-pil.imagetk python3-serial python3-zope.component python3-pystray Configuration ============= @@ -53,9 +73,9 @@ The mapping .. code:: ini [mapping] - Mozilla Firefox = Firefox - Teams = Teams - irssi = Irssi + Mozilla Firefox = firefox.json + Teams = teams.json + irssi = irssi.json … Mapping list @@ -83,24 +103,48 @@ The application send a json string to the endpoint (network or serial connection .. code:: json - {"layout": "Firefox"} + {"layer_name": "keymap"} -This application does not handle the code in the keyboard. CircuitPython -provide a native library in order `to read or store json`_, and firmware build -upon it (like KMK) are easer to use with. +the keymap can be: -.. _`to read or store json`: https://docs.circuitpython.org/en/latest/docs/library/json.html +:a string: -Reading message ---------------- + The key name will be sent: :json:`"A"` + +:a list: -The endpoint can also send a message to the application. For now, the message -is a raw string with the name of the layer. + All the keys will be chained in a single stroke: :json:`["^", "A"]` -When the application receive such message, it will look for the active window -in the desktop, and register the application with this layer. If this -application is selected again, the application will ask the endpoint to switch -to the same layer again. +:a dictionnary: + + Used to create custom sequences: :json:`{"seq": ["/", "W", "C", "ENTER"]}` + +:null: + + The key will do nothing. + +.. code:: json + + { "Example": [ + {"seq": ["A", "B", "C"]}, ["^", "R"], ["^", "T"], ["^", "W"], + null, null, null, null + ]} + +CircuitPython provide a native library in order `to read or store json`_, and +firmware build upon it (like KMK) are easer to use with. + +.. _`to read or store json`: https://docs.circuitpython.org/en/latest/docs/library/json.html + +.. Reading message +.. --------------- +.. +.. The endpoint can also send a message to the application. For now, the message +.. is a raw string with the name of the layer. +.. +.. When the application receive such message, it will look for the active window +.. in the desktop, and register the application with this layer. If this +.. application is selected again, the application will ask the endpoint to switch +.. to the same layer again. Network connection ================== @@ -108,3 +152,7 @@ Network connection You can relay the events to another one instance using the network. I'm using this when I'm connected over VNC in order to use the keyboard as if it was plugged directly in the host. + +---------- +The device +---------- diff --git a/socket_conn.py b/socket_conn.py index 0ac7230..433cc75 100755 --- a/socket_conn.py +++ b/socket_conn.py @@ -44,3 +44,4 @@ class SocketConnection(object): """ Write into the connection. Raise an exception if disconnected """ self.s.sendall(content) + self.s.sendall(bytes("\n", "utf-8")) diff --git a/win32.py b/win32.py index 46d1efe..bff980b 100755 --- a/win32.py +++ b/win32.py @@ -1,6 +1,6 @@ # Required for the window title name from ctypes import wintypes, windll, create_unicode_buffer, WINFUNCTYPE -from typing import Self, Optional, Dict +from typing import Optional, Dict from zope import interface from interfaces import desktopEvent @@ -51,12 +51,12 @@ def setHook(WinEventProc, eventType): @interface.implementer(desktopEvent.IDesktop) class Listener(object): - def __init__(self: Self, mapping: Dict[str, str], queue): + def __init__(self, mapping: Dict[str, str], queue): self.WinEventProc = WinEventProcType(self.callback) self.mapping = mapping self.queue = queue - def getForegroundWindowTitle(self: Self) -> Optional[str]: + def getForegroundWindowTitle(self) -> Optional[str]: """ Get the window title name. Example found from https://stackoverflow.com/a/58355052 See the function in the winuser librarry : @@ -73,7 +73,7 @@ class Listener(object): else: return None - def callback(self: Self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, + def callback(self, hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) -> None: if hwnd != windll.user32.GetForegroundWindow(): @@ -88,17 +88,21 @@ class Listener(object): return title = str(title.value).lower() for pattern, code in self.mapping.items(): + if pattern == "default": + continue if pattern in title: self.queue.put ( (code, None) ) return - self.queue.put ( ("Windows", None) ) + default = self.mapping.get("default", None) + if default is not None: + self.queue.put ( (default, None) ) - def start(self: Self) -> None: + def start(self) -> None: self.hookIDs = [setHook(self.WinEventProc, et) for et in eventTypes.keys()] if not any(self.hookIDs): print('SetWinEventHook failed') sys.exit(1) - def stop(self: Self) -> None: + def stop(self) -> None: for hook in self.hookIDs: windll.user32.UnhookWinEvent(hook) diff --git a/xlib.py b/xlib.py index b86a8b1..3e43e51 100644 --- a/xlib.py +++ b/xlib.py @@ -36,27 +36,47 @@ class Listener(Thread): component.handle(Debug("Waiting for xlib event")) self.running = True while self.running: + event = disp.next_event() try: window_id = root.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0] window = disp.create_resource_object('window', window_id) window.change_attributes(event_mask=Xlib.X.PropertyChangeMask) - window_name = str(window.get_full_property(NET_WM_NAME, 0).value).lower() + window_prop = window.get_full_property(NET_WM_NAME, 0) + if window_prop is None: + continue + window_name = str(window_prop.value).lower() class_name = str(window.get_full_property(WM_CLASS, 0).value).lower() except Xlib.error.XError: - window_name = None - class_name = None + continue - if window_name is not None and window_name != self.active_window: + if window_name is None or window_name == self.active_window: + continue - self.active_window = window_name - for pattern, code in self.mapping.items(): - if (pattern in window_name or pattern in class_name) and code != self.last_code: + self.active_window = window_name + found = False + for pattern, code in self.mapping.items(): + # Ignore the default layout + if pattern == "default": + continue + if not (pattern in window_name or pattern in class_name): + continue + # We found something. At this point, even if we do not update + # the layer (because it is the same as the previous) we do not + # switch back to the default layer. + found = True + if code != self.last_code: - component.handle(Debug("Switching to '%s' for '%s'" % (code, window_name))) - self.queue.put ( (code, None) ) - self.last_code = code - break - event = disp.next_event() + component.handle(Debug("Switching to '%s' for '%s'" % (code, window_name))) + self.queue.put ( (code, None) ) + self.last_code = code + break + if not found and self.last_code != "default": + default = self.mapping.get("default", None) + if default is None: + continue + + self.queue.put ( (default, None) ) + self.last_code = "default" def stop(self): self.running = False -- cgit v1.2.3