MP-Server/mp-server.py
2025-03-14 22:22:20 -07:00

261 lines
9.8 KiB
Python

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(
"<Configure>",
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()