diff options
author | Sébastien Dailly <sebastien@dailly.me> | 2023-07-15 14:44:41 +0200 |
---|---|---|
committer | Sébastien Dailly <sebastien@dailly.me> | 2023-07-15 14:59:43 +0200 |
commit | 02d676bda89c2fb8469ea81f7429c19c1e29df7c (patch) | |
tree | 11dae86a561a4800661e6c174b47611a704f857e /macropad.pyw |
Initial commit
Diffstat (limited to 'macropad.pyw')
-rwxr-xr-x | macropad.pyw | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/macropad.pyw b/macropad.pyw new file mode 100755 index 0000000..f35e141 --- /dev/null +++ b/macropad.pyw @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 + +import serial + +import tkinter as tk +from tkinter import Text +import pystray +from pystray import MenuItem as item +from PIL import Image, ImageTk + +import threading +from os import path +import sys + +from zope import component +import interfaces +from interfaces import endpoint +from interfaces.message import IMessage, Debug + +import configparser + +script_path = path.dirname(path.realpath(__file__)) +config_file = path.join(script_path, "config.ini") + +config = configparser.ConfigParser(delimiters="=") +config.read(config_file) + + + +init_mapping = config["mapping"] +mapping = dict(init_mapping) + +from queue import Queue +q = Queue() + +component.provideAdapter(interfaces.endpoint.EndPoint) + +# +# Guess the platform and the load the corresponding event listener +# + +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) + +elif platform == 'linux': + import xlib + xlib_listener = xlib.Listener(mapping, q) + xlib_listener.start() + component.provideUtility(xlib_listener, interfaces.desktopEvent.IDesktop) + +# +# How to connect to the peripherical +# + +if config.has_section("connection.serial"): + + from serial_conn import SerialConnection + endpoint = component.queryAdapter(SerialConnection(config["connection.serial"]), endpoint.IEndpoint) + endpoint.queue = q + endpoint.connect() + component.provideUtility(endpoint, interfaces.endpoint.IEndpoint) + +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) + +if config.has_section("socket.serve"): + + import socketserver + server = socketserver.Handler(config["socket.serve"], q) + +else: + + server = None + + +class Icon(object): + + def __init__(self, image): + menu=( + item('Quit', self.quit_window), + item('Show', self.show_window, default=True), + item('Reset',self.reset), + ) + self.icon=pystray.Icon("name", image, "Macropad companion", menu) + + self.stop = threading.Event() + self.show_hide = threading.Event() + + # 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 + + def start(self): + self.icon_thread.start() + + def quit(self): + self.icon.stop() + + def quit_window(self): + self.stop.set() + + def show_window(self): + self.show_hide.set() + + def reset(self): + # Create a copy of the dictonnary before updating the keys + tmp_mapping = dict(mapping) + for key in tmp_mapping.keys() : + if key not in init_mapping.keys(): + del mapping[key] + +import json +class Application(object): + + def __init__(self): + self.window = tk.Tk() + self.text = Text(self.window, height=8) + ## State of the #pplication + self.running = True + self.visible = False + self.focused_window = None + self.last_layout = None + + component.provideHandler(self.log) + + # Window property + self.window.withdraw() + self.window.title("Macropad companion") + icon = path.join(script_path, "favicon.ico") + try: + self.window.iconbitmap(icon) + except: + pass + self.text.pack() + + # When closing, return back to the iconified mode + self.window.protocol("WM_DELETE_WINDOW", self.hide) + # Start the application in iconified mode + image=Image.open(icon) + self.icon = Icon(image) + self.icon.start() + component.handle(Debug("Started")) + + def hide(self): + self.icon.show_hide.clear() + self.visible = False + self.window.withdraw() + self.update() + + + def update(self): + if self.icon.stop.is_set(): + self.icon.quit() + self.running = False + self.window.destroy() + component.queryUtility(interfaces.desktopEvent.IDesktop).stop() + return + + if self.icon.show_hide.is_set(): + if not self.visible: + self.window.deiconify() + else: + self.window.withdraw() + self.icon.show_hide.clear() + self.visible = not self.visible + + @component.adapter(IMessage) + def log(self, message : str): + print(message.content) + try: + f = open("/tmp/macropad.log", "a") + f.write(message.content) + f.write("\n") + f.close() + self.text.insert("1.0", "\n") + self.text.insert("1.0", message.content) + self.text.delete("200.0", "end") + except Exception as e: + print(e) + + def send(self, data: str): + if data == self.last_layout: + 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) + + def associate(self, layout: str, name: str): + mapping[name] = layout + component.handle(Debug("Associating %s with %s" % (name, layout))) + + def exec(self): + try: + self.update() + if server is not None: server.update() + conn = component.queryUtility(interfaces.endpoint.IEndpoint) + if not conn.isConnected(): + component.handle(Debug("Reconnecting…")) + + conn.state = conn.STATE_CONNECTING + self.window.after(5000, conn.connect) + else: + conn.fetch() + except Exception as e: + component.handle(Debug( str(e) )) + print(e) + # Got any error, stop the application properly + self.icon.stop.set() + + if app.running: + self.window.after(200, self.exec) + + while not q.empty(): + last_layout, app_ = q.get(False) + self.send(last_layout) + if app_ is not None: self.associate(last_layout, app_) + +if __name__ == '__main__': + app = Application() + + # Initialize the main loop + app.exec() + try: + app.window.mainloop() + except: + app.running = False + app.window.destroy() + app.icon.quit() + component.queryUtility(interfaces.desktopEvent.IDesktop).stop() |