From c8971950839e72f5cc5d4a3639d95e5b04575fc9 Mon Sep 17 00:00:00 2001 From: jknapp Date: Fri, 14 Mar 2025 22:22:20 -0700 Subject: [PATCH] first revision --- mp-server.py | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 mp-server.py diff --git a/mp-server.py b/mp-server.py new file mode 100644 index 0000000..9f50910 --- /dev/null +++ b/mp-server.py @@ -0,0 +1,260 @@ +import tkinter as tk +from tkinter import filedialog, simpledialog +import json +import socket +import threading +import base64 +import os +import pyautogui +import subprocess +from PIL import Image, ImageTk + +class MacroServer: + def __init__(self, root): + self.root = root + self.root.title("MacroPad Server") + self.root.geometry("800x600") + + self.macros = {} + self.load_macros() + + # Create GUI + self.create_widgets() + + # Start server thread + self.server_running = True + self.server_thread = threading.Thread(target=self.run_server) + self.server_thread.daemon = True + self.server_thread.start() + + def create_widgets(self): + # Toolbar + toolbar = tk.Frame(self.root) + toolbar.pack(side=tk.TOP, fill=tk.X) + + tk.Button(toolbar, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=5, pady=5) + tk.Button(toolbar, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=5, pady=5) + + # Macro list frame + list_frame = tk.Frame(self.root) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Scrollable canvas for macros + self.canvas = tk.Canvas(list_frame) + scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview) + self.scrollable_frame = tk.Frame(self.canvas) + + self.scrollable_frame.bind( + "", + lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")) + ) + + self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") + self.canvas.configure(yscrollcommand=scrollbar.set) + + self.canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + self.display_macros() + + # Status bar + self.status_var = tk.StringVar() + self.status_var.set("Server ready. Waiting for connections...") + status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + + def load_macros(self): + try: + if os.path.exists("macros.json"): + with open("macros.json", "r") as f: + self.macros = json.load(f) + except Exception as e: + print(f"Error loading macros: {e}") + self.macros = {} + + def save_macros(self): + try: + with open("macros.json", "w") as f: + json.dump(self.macros, f) + except Exception as e: + print(f"Error saving macros: {e}") + + def display_macros(self): + # Clear existing macros + for widget in self.scrollable_frame.winfo_children(): + widget.destroy() + + # Display macros in a grid + row, col = 0, 0 + max_cols = 3 + + for macro_id, macro in self.macros.items(): + frame = tk.Frame(self.scrollable_frame, bd=2, relief=tk.RAISED, padx=5, pady=5) + frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") + + # Load image if exists + if "image_data" in macro and macro["image_data"]: + try: + image_data = base64.b64decode(macro["image_data"]) + image = Image.open(io.BytesIO(image_data)) + image = image.resize((64, 64), Image.LANCZOS) + photo = ImageTk.PhotoImage(image) + label = tk.Label(frame, image=photo) + label.image = photo # Keep a reference + label.pack() + except Exception as e: + print(f"Error displaying image: {e}") + tk.Label(frame, text="[No Image]", width=8, height=4).pack() + else: + tk.Label(frame, text="[No Image]", width=8, height=4).pack() + + tk.Label(frame, text=macro["name"]).pack() + + col += 1 + if col >= max_cols: + col = 0 + row += 1 + + def add_macro(self): + # Create a dialog to add a new macro + dialog = tk.Toplevel(self.root) + dialog.title("Add Macro") + dialog.geometry("400x300") + dialog.transient(self.root) + + tk.Label(dialog, text="Macro Name:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + name_entry = tk.Entry(dialog, width=30) + name_entry.grid(row=0, column=1, padx=5, pady=5) + + tk.Label(dialog, text="Type:").grid(row=1, column=0, padx=5, pady=5, sticky="w") + type_var = tk.StringVar(value="text") + tk.Radiobutton(dialog, text="Text", variable=type_var, value="text").grid(row=1, column=1, sticky="w") + tk.Radiobutton(dialog, text="Application", variable=type_var, value="app").grid(row=2, column=1, sticky="w") + + tk.Label(dialog, text="Command/Text:").grid(row=3, column=0, padx=5, pady=5, sticky="w") + command_text = tk.Text(dialog, width=30, height=5) + command_text.grid(row=3, column=1, padx=5, pady=5) + + image_path = tk.StringVar() + tk.Label(dialog, text="Image:").grid(row=4, column=0, padx=5, pady=5, sticky="w") + tk.Entry(dialog, textvariable=image_path, width=30).grid(row=4, column=1, padx=5, pady=5) + tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename( + filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")]))).grid(row=4, column=2) + + def save_macro(): + name = name_entry.get().strip() + if not name: + tk.messagebox.showerror("Error", "Macro name is required") + return + + macro_type = type_var.get() + command = command_text.get("1.0", tk.END).strip() + + # Generate a unique ID + macro_id = str(len(self.macros) + 1) + + # Process image + image_data = "" + img_path = image_path.get() + if img_path: + try: + with open(img_path, "rb") as img_file: + image_data = base64.b64encode(img_file.read()).decode('utf-8') + except Exception as e: + print(f"Error processing image: {e}") + + # Create macro + self.macros[macro_id] = { + "name": name, + "type": macro_type, + "command": command, + "image_data": image_data + } + + self.save_macros() + self.display_macros() + dialog.destroy() + + tk.Button(dialog, text="Save", command=save_macro).grid(row=5, column=0, padx=5, pady=20) + tk.Button(dialog, text="Cancel", command=dialog.destroy).grid(row=5, column=1, padx=5, pady=20) + + def edit_macro(self): + # To be implemented - similar to add_macro but pre-fills fields with existing data + pass + + def delete_macro(self): + # To be implemented - offers a selection and deletes the chosen macro + pass + + def execute_macro(self, macro_id): + if macro_id not in self.macros: + return False + + macro = self.macros[macro_id] + try: + if macro["type"] == "text": + pyautogui.typewrite(macro["command"]) + elif macro["type"] == "app": + subprocess.Popen(macro["command"], shell=True) + return True + except Exception as e: + print(f"Error executing macro: {e}") + return False + + def run_server(self): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + server_socket.bind(('0.0.0.0', 40000)) + server_socket.listen(5) + + while self.server_running: + client_socket, address = server_socket.accept() + self.status_var.set(f"Client connected from {address}") + + client_thread = threading.Thread(target=self.handle_client, args=(client_socket,)) + client_thread.daemon = True + client_thread.start() + + except Exception as e: + print(f"Server error: {e}") + finally: + server_socket.close() + + def handle_client(self, client_socket): + try: + while self.server_running: + data = client_socket.recv(1024).decode('utf-8') + if not data: + break + + try: + request = json.loads(data) + if request['action'] == 'get_macros': + # Send all macros to client + client_socket.send(json.dumps(self.macros).encode('utf-8')) + elif request['action'] == 'execute': + # Execute the specified macro + success = self.execute_macro(request['macro_id']) + client_socket.send(json.dumps({'success': success}).encode('utf-8')) + except json.JSONDecodeError: + print("Invalid JSON received") + + except Exception as e: + print(f"Error handling client: {e}") + finally: + client_socket.close() + + def on_closing(self): + self.server_running = False + self.save_macros() + self.root.destroy() + +if __name__ == "__main__": + import io # Added for BytesIO + root = tk.Tk() + app = MacroServer(root) + root.protocol("WM_DELETE_WINDOW", app.on_closing) + root.mainloop()