aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@dailly.me>2023-08-27 16:41:10 +0200
committerSébastien Dailly <sebastien@dailly.me>2023-08-27 16:41:10 +0200
commitfcce9177e356bb27283926451433130a8809fcb0 (patch)
treeff1aecf99be053f1681bd8965ae3f33c2a42169f
parent02d676bda89c2fb8469ea81f7429c19c1e29df7c (diff)
Send the whole keymap to the device
-rw-r--r--.gitignore1
-rwxr-xr-xmacropad.pyw33
-rwxr-xr-xreadme.rst84
-rwxr-xr-xsocket_conn.py1
-rwxr-xr-xwin32.py18
-rw-r--r--xlib.py44
6 files changed, 129 insertions, 52 deletions
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