import tkinter as tk from tkinter import filedialog, ttk import json import socket import threading import os import pyautogui import subprocess from PIL import Image, ImageTk import pystray # Add this import for system tray functionality import uuid class MacroPadServer: def __init__(self, root): self.root = root self.root.title("MacroPad Server") self.root.geometry("800x600") # Create the image Directory self.images_dir = "macro_images" os.makedirs(self.images_dir, exist_ok=True) # Set dark theme colors self.bg_color = "#2E2E2E" self.fg_color = "#FFFFFF" self.highlight_color = "#3D3D3D" self.accent_color = "#007BFF" self.button_bg = "#444444" self.button_fg = "#FFFFFF" # Apply theme self.root.configure(bg=self.bg_color) self.style = ttk.Style() self.style.theme_use('clam') # Configure styles self.style.configure('TFrame', background=self.bg_color) self.style.configure('TLabel', background=self.bg_color, foreground=self.fg_color) self.style.configure('TButton', background=self.button_bg, foreground=self.button_fg, borderwidth=0) self.style.map('TButton', background=[('active', self.accent_color), ('disabled', self.highlight_color)], foreground=[('active', self.button_fg), ('disabled', '#999999')]) self.style.configure('TRadiobutton', background=self.bg_color, foreground=self.fg_color) 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() # Set up system tray icon self.setup_tray_icon() # Capture the window close event self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # Capture the window minimize event self.root.bind("", lambda e: self.minimize_to_tray() if self.root.state() == 'iconic' else None) def setup_tray_icon(self): # Create a simple icon for the tray icon_image = Image.new("RGB", (64, 64), color=self.accent_color) # Menu for the tray icon menu = ( pystray.MenuItem('Show', self.show_window), pystray.MenuItem('Exit', self.exit_app) ) self.icon = pystray.Icon("macropad_server", icon_image, "MacroPad Server", menu) def minimize_to_tray(self): self.root.withdraw() # Hide window if not self.icon.visible: # Start the tray icon if it's not already running self.icon_thread = threading.Thread(target=self.icon.run) self.icon_thread.daemon = True self.icon_thread.start() def show_window(self, icon=None, item=None): self.root.deiconify() # Show window self.root.lift() # Bring window to front self.root.focus_force() # Focus the window if self.icon.visible: self.icon.stop() # Remove icon def exit_app(self, icon=None, item=None): if self.icon.visible: self.icon.stop() self.server_running = False self.save_macros() self.root.destroy() def create_widgets(self): # Toolbar toolbar = tk.Frame(self.root, bg=self.highlight_color) toolbar.pack(side=tk.TOP, fill=tk.X) # Custom button style button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'padx': 10, 'pady': 5, 'bd': 0, 'activebackground': self.accent_color, 'activeforeground': self.button_fg, 'relief': tk.FLAT} tk.Button(toolbar, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) tk.Button(toolbar, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) tk.Button(toolbar, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=5, pady=5) tk.Button(toolbar, text="Minimize to Tray", command=self.minimize_to_tray, **button_style).pack(side=tk.LEFT, padx=5, pady=5) # Macro list frame list_frame = tk.Frame(self.root, bg=self.bg_color) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Scrollable canvas for macros self.canvas = tk.Canvas(list_frame, bg=self.bg_color, highlightthickness=0) scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=self.canvas.yview) self.scrollable_frame = tk.Frame(self.canvas, bg=self.bg_color) 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, bg=self.highlight_color, fg=self.fg_color) 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, bg=self.highlight_color) frame.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") # Load image if exists if "image_path" in macro and macro["image_path"]: try: img_path = macro["image_path"] img = Image.open(img_path) # Resize for display img.thumbnail((64, 64)) # Keep aspect ratio for display photo = ImageTk.PhotoImage(img) label = tk.Label(frame, image=photo, bg=self.highlight_color) 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, bg=self.highlight_color, fg=self.fg_color).pack() else: tk.Label(frame, text="[No Image]", width=8, height=4, bg=self.highlight_color, fg=self.fg_color).pack() tk.Label(frame, text=macro["name"], bg=self.highlight_color, fg=self.fg_color).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("450x350") # Increased height for additional options dialog.transient(self.root) dialog.configure(bg=self.bg_color) # Apply dark theme to dialog tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w") name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) name_entry.grid(row=0, column=1, padx=5, pady=5) tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w") type_var = tk.StringVar(value="text") radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color} tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w") tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w") tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w") command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) command_text.grid(row=3, column=1, padx=5, pady=5) # Key modifiers frame mod_frame = tk.Frame(dialog, bg=self.bg_color) mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5) tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5) # Add checkboxes for modifiers ctrl_var = tk.BooleanVar(value=False) alt_var = tk.BooleanVar(value=False) shift_var = tk.BooleanVar(value=False) enter_var = tk.BooleanVar(value=False) checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color, 'activebackground': self.bg_color, 'activeforeground': self.fg_color} tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5) image_path = tk.StringVar() tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w") tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color).grid(row=5, column=1, padx=5, pady=5) button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color, 'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT} tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename( filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, 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_path_reference = "" img_path = image_path.get() if img_path: try: # Generate unique filename for the image file_ext = os.path.splitext(img_path)[1].lower() unique_filename = f"{uuid.uuid4().hex}{file_ext}" dest_path = os.path.join(self.images_dir, unique_filename) # Resize image to max 256x256 with Image.open(img_path) as img: img.thumbnail((256, 256)) img.save(dest_path) # Store the relative path to the image image_path_reference = os.path.join("macro_images", unique_filename) except Exception as e: print(f"Error processing image: {e}") # Create macro with modifier keys self.macros[macro_id] = { "name": name, "type": macro_type, "command": command, "image_path": image_path_reference, "modifiers": { "ctrl": ctrl_var.get(), "alt": alt_var.get(), "shift": shift_var.get(), "enter": enter_var.get() } } self.save_macros() self.display_macros() dialog.destroy() tk.Button(dialog, text="Save", command=save_macro, **button_style).grid(row=6, column=0, padx=5, pady=20) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20) def edit_macro(self): # First, show a dialog to select which macro to edit if not self.macros: tk.messagebox.showinfo("No Macros", "There are no macros to edit.") return dialog = tk.Toplevel(self.root) dialog.title("Select Macro to Edit") dialog.geometry("200x340") dialog.transient(self.root) dialog.configure(bg=self.bg_color) # Create a listbox to show available macros tk.Label(dialog, text="Select a macro to edit:", bg=self.bg_color, fg=self.fg_color).pack(pady=5) listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color) listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # Populate the listbox with macro names macro_ids = [] for macro_id, macro in self.macros.items(): listbox.insert(tk.END, macro["name"]) macro_ids.append(macro_id) def on_select(): if not listbox.curselection(): tk.messagebox.showwarning("No Selection", "Please select a macro to edit.") return idx = listbox.curselection()[0] selected_macro_id = macro_ids[idx] dialog.destroy() self.open_edit_dialog(selected_macro_id) button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color, 'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT} tk.Button(dialog, text="Edit Selected", command=on_select, **button_style).pack(pady=10) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5) def open_edit_dialog(self, macro_id): # Open dialog to edit the selected macro macro = self.macros[macro_id] dialog = tk.Toplevel(self.root) dialog.title("Edit Macro") dialog.geometry("450x350") # Increased height for additional options dialog.transient(self.root) dialog.configure(bg=self.bg_color) # Apply dark theme to dialog tk.Label(dialog, text="Macro Name:", bg=self.bg_color, fg=self.fg_color).grid(row=0, column=0, padx=5, pady=5, sticky="w") name_entry = tk.Entry(dialog, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) name_entry.grid(row=0, column=1, padx=5, pady=5) name_entry.insert(0, macro["name"]) tk.Label(dialog, text="Type:", bg=self.bg_color, fg=self.fg_color).grid(row=1, column=0, padx=5, pady=5, sticky="w") type_var = tk.StringVar(value=macro["type"]) radio_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color} tk.Radiobutton(dialog, text="Text", variable=type_var, value="text", **radio_style).grid(row=1, column=1, sticky="w") tk.Radiobutton(dialog, text="Application", variable=type_var, value="app", **radio_style).grid(row=2, column=1, sticky="w") tk.Label(dialog, text="Command/Text:", bg=self.bg_color, fg=self.fg_color).grid(row=3, column=0, padx=5, pady=5, sticky="w") command_text = tk.Text(dialog, width=30, height=5, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) command_text.grid(row=3, column=1, padx=5, pady=5) command_text.insert("1.0", macro["command"]) # Modifiers frame mod_frame = tk.Frame(dialog, bg=self.bg_color) mod_frame.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=5) tk.Label(mod_frame, text="Key Modifiers:", bg=self.bg_color, fg=self.fg_color).pack(side=tk.LEFT, padx=5) # Get existing modifiers or set defaults modifiers = macro.get("modifiers", {}) # Add checkboxes for modifiers ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False)) alt_var = tk.BooleanVar(value=modifiers.get("alt", False)) shift_var = tk.BooleanVar(value=modifiers.get("shift", False)) enter_var = tk.BooleanVar(value=modifiers.get("enter", False)) checkbox_style = {'bg': self.bg_color, 'fg': self.fg_color, 'selectcolor': self.accent_color, 'activebackground': self.bg_color, 'activeforeground': self.fg_color} tk.Checkbutton(mod_frame, text="Ctrl", variable=ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Alt", variable=alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Shift", variable=shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5) tk.Checkbutton(mod_frame, text="Add Enter", variable=enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5) image_path = tk.StringVar() tk.Label(dialog, text="Image:", bg=self.bg_color, fg=self.fg_color).grid(row=5, column=0, padx=5, pady=5, sticky="w") image_entry = tk.Entry(dialog, textvariable=image_path, width=30, bg=self.highlight_color, fg=self.fg_color, insertbackground=self.fg_color) image_entry.grid(row=5, column=1, padx=5, pady=5) button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color, 'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT} tk.Button(dialog, text="Browse...", command=lambda: image_path.set(filedialog.askopenfilename( filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])), **button_style).grid(row=5, column=2) def save_edited_macro(): name = name_entry.get().strip() if not name: tk.messagebox.showerror("Error", "Macro name is required") return new_type = type_var.get() command = command_text.get("1.0", tk.END).strip() # Keep the old image or update with new one image_path_reference = macro.get("image_path", "") img_path = image_path.get() if img_path: try: # Generate unique filename for the image file_ext = os.path.splitext(img_path)[1].lower() unique_filename = f"{uuid.uuid4().hex}{file_ext}" dest_path = os.path.join(self.images_dir, unique_filename) # Resize image to max 256x256 with Image.open(img_path) as img: img.thumbnail((256, 256)) img.save(dest_path) # Store the relative path to the image image_path_reference = os.path.join("macro_images", unique_filename) except Exception as e: print(f"Error processing image: {e}") # Update macro with modifiers self.macros[macro_id] = { "name": name, "type": new_type, "command": command, "image_path": image_path_reference, "modifiers": { "ctrl": ctrl_var.get(), "alt": alt_var.get(), "shift": shift_var.get(), "enter": enter_var.get() } } self.save_macros() self.display_macros() dialog.destroy() tk.Button(dialog, text="Save", command=save_edited_macro, **button_style).grid(row=6, column=0, padx=5, pady=20) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).grid(row=6, column=1, padx=5, pady=20) def delete_macro(self): # Show a dialog to select which macro to delete if not self.macros: tk.messagebox.showinfo("No Macros", "There are no macros to delete.") return dialog = tk.Toplevel(self.root) dialog.title("Delete Macro") dialog.geometry("200x340") dialog.transient(self.root) dialog.configure(bg=self.bg_color) # Create a listbox to show available macros tk.Label(dialog, text="Select a macro to delete:", bg=self.bg_color, fg=self.fg_color).pack(pady=5) listbox = tk.Listbox(dialog, bg=self.highlight_color, fg=self.fg_color, selectbackground=self.accent_color) listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # Populate the listbox with macro names macro_ids = [] for macro_id, macro in self.macros.items(): listbox.insert(tk.END, macro["name"]) macro_ids.append(macro_id) def on_delete(): if not listbox.curselection(): tk.messagebox.showwarning("No Selection", "Please select a macro to delete.") return idx = listbox.curselection()[0] selected_macro_id = macro_ids[idx] selected_name = self.macros[selected_macro_id]["name"] # Confirm deletion if tk.messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{selected_name}'?"): # Delete associated image file if it exists macro = self.macros[selected_macro_id] if "image_path" in macro and macro["image_path"]: try: img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"]) if os.path.exists(img_path): os.remove(img_path) except Exception as e: print(f"Error removing image file: {e}") del self.macros[selected_macro_id] self.save_macros() self.display_macros() dialog.destroy() button_style = {'bg': self.button_bg, 'fg': self.button_fg, 'activebackground': self.accent_color, 'activeforeground': self.button_fg, 'bd': 0, 'relief': tk.FLAT} tk.Button(dialog, text="Delete Selected", command=on_delete, **button_style).pack(pady=10) tk.Button(dialog, text="Cancel", command=dialog.destroy, **button_style).pack(pady=5) 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": # Handle key modifiers modifiers = macro.get("modifiers", {}) keys_to_press = [] # Add modifier keys if enabled if modifiers.get("ctrl", False): keys_to_press.append('ctrl') if modifiers.get("alt", False): keys_to_press.append('alt') if modifiers.get("shift", False): keys_to_press.append('shift') # If there are modifier keys, use hotkey functionality if keys_to_press: # For single characters with modifiers, use hotkey if len(macro["command"]) == 1: keys_to_press.append(macro["command"], interval=0.02) pyautogui.hotkey(*keys_to_press) else: # For longer text, press modifiers, type text, then release for key in keys_to_press: pyautogui.keyDown(key) pyautogui.typewrite(macro["command"], interval=0.02) for key in reversed(keys_to_press): pyautogui.keyUp(key) else: # No modifiers, just type the text pyautogui.typewrite(macro["command"], interval=0.02) # Add Enter/Return if requested if modifiers.get("enter", False): pyautogui.press('enter') 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 = b"" while True: chunk = client_socket.recv(4096) if not chunk: break data += chunk try: # Try to see if we've received a complete JSON object json_data = json.loads(data.decode('utf-8')) break # If successful, break out of the inner loop except json.JSONDecodeError: # If it's not a complete JSON object yet, keep reading continue if not data: break try: request = json_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']) response = {'success': success} client_socket.send(json.dumps(response).encode('utf-8')) elif request['action'] == 'get_image': image_path = request['image_path'] if os.path.exists(image_path): try: with open(image_path, 'rb') as f: image_data =f.read() image_size = len(image_data) client_socket.send(str(image_size).encode('utf-8')) client_socket.recv(1024) # Wait for acknowledgment client_socket.sendall(image_data) except FileNotFoundError: client_socket.send(b"ERROR: Image not found") except Exception as e: client_socket.send(f"ERROR: {str(e)}".encode('utf-8')) else: client_socket.send(json.dumps({'error': 'Unknown action'}).encode('utf-8')) except (json.JSONDecodeError, KeyError, TypeError) as e: error_msg = {'error': f'Invalid request: {str(e)}'} client_socket.send(json.dumps(error_msg).encode('utf-8')) print(f"JSON processing error: {e}, Data received: {data[:100]}") except Exception as e: print(f"Client handling error: {e}") finally: client_socket.close() self.status_var.set("Client disconnected. Waiting for connections...") def on_closing(self): # When the window is closed, minimize to tray instead of closing self.minimize_to_tray() if __name__ == "__main__": root = tk.Tk() app = MacroPadServer(root) root.mainloop()