diff --git a/dist/mp-server-v2.exe b/dist/mp-server-v2.exe new file mode 100644 index 0000000..04b005a Binary files /dev/null and b/dist/mp-server-v2.exe differ diff --git a/mp-server-v2.py b/mp-server-v2.py new file mode 100644 index 0000000..f6ab0ea --- /dev/null +++ b/mp-server-v2.py @@ -0,0 +1,929 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import json +import os +import uuid +import pyautogui +import subprocess +import threading +import pystray +from PIL import Image, ImageTk +from flask import Flask, render_template_string, request, jsonify, send_file +import webbrowser +from waitress import serve +import logging +import netifaces + +class MacroPadServer: + def __init__(self, root): + self.root = root + self.root.title("MacroPad Server") + self.root.geometry("800x600") + self.configure_styles() + + # Set up directories + base_dir = os.path.dirname(os.path.abspath(__file__)) + self.data_file = os.path.join(base_dir, "macros.json") + self.images_dir = os.path.join(base_dir, "macro_images") + os.makedirs(self.images_dir, exist_ok=True) + + self.macros = {} + self.load_macros() + self.image_cache = {} # Cache for images + + # Set up server + self.server_running = False + self.web_app = None + self.flask_thread = None + self.server_port = 40000 + + # Create UI + self.create_ui() + + # Set up window close handler + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + def configure_styles(self): + # Dark theme + self.bg_color = "#2e2e2e" + self.fg_color = "#ffffff" + self.highlight_color = "#3e3e3e" + self.accent_color = "#007acc" + self.button_bg = "#505050" + self.button_fg = "#ffffff" + + self.root.configure(bg=self.bg_color) + + # Configure ttk styles + style = ttk.Style() + style.theme_use("clam") + style.configure("TButton", background=self.button_bg, foreground=self.button_fg) + style.map("TButton", background=[("active", self.accent_color)]) + style.configure("TFrame", background=self.bg_color) + style.configure("TLabel", background=self.bg_color, foreground=self.fg_color) + + def create_ui(self): + # Create main container + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Left side: Macro list and buttons + left_frame = ttk.Frame(main_frame) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Macro list + self.listbox_frame = ttk.Frame(left_frame) + self.listbox_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) + + # Right side: Details and server controls + right_frame = ttk.Frame(main_frame) + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0)) + + # Button container + button_frame = ttk.Frame(left_frame) + button_frame.pack(fill=tk.X, pady=(0, 10)) + + button_style = {'style': 'TButton'} + + # Add buttons + ttk.Button(button_frame, text="Add Macro", command=self.add_macro, **button_style).pack(side=tk.LEFT, padx=2) + ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro, **button_style).pack(side=tk.LEFT, padx=2) + ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro, **button_style).pack(side=tk.LEFT, padx=2) + + # Server controls + server_frame = ttk.Frame(right_frame) + server_frame.pack(fill=tk.X, pady=10) + + ttk.Label(server_frame, text="Web Server Control:").grid(row=0, column=0, sticky="w", pady=5) + + self.server_button = ttk.Button(server_frame, text="Start Web Server", command=self.toggle_server) + self.server_button.grid(row=0, column=1, padx=5, pady=5) + + # Status display + self.status_var = tk.StringVar(value="Web server not running") + ttk.Label(server_frame, textvariable=self.status_var).grid(row=1, column=0, columnspan=2, sticky="w", pady=5) + + # QR code placeholder + self.qr_label = ttk.Label(right_frame) + self.qr_label.pack(pady=10) + + # Server URL display + self.url_var = tk.StringVar(value="") + self.url_label = ttk.Label(right_frame, textvariable=self.url_var) + self.url_label.pack(pady=5) + + # Open in browser button + self.browser_button = ttk.Button(right_frame, text="Open in Browser", + command=self.open_in_browser, state=tk.DISABLED) + self.browser_button.pack(pady=5) + + # Display any existing macros + self.display_macros() + + def load_macros(self): + try: + if os.path.exists(self.data_file): + with open(self.data_file, "r") as file: + self.macros = json.load(file) + except Exception as e: + print(f"Error loading macros: {e}") + self.macros = {} + + def save_macros(self): + try: + with open(self.data_file, "w") as file: + json.dump(self.macros, file, indent=4) + except Exception as e: + print(f"Error saving macros: {e}") + + def display_macros(self): + # Clear the previous listbox + for widget in self.listbox_frame.winfo_children(): + widget.destroy() + + # Create scrollable canvas for macros + canvas = tk.Canvas(self.listbox_frame, bg=self.bg_color, highlightthickness=0) + scrollbar = ttk.Scrollbar(self.listbox_frame, orient="vertical", command=canvas.yview) + scrollable_frame = ttk.Frame(canvas) + + # Configure canvas and scrollable frame + scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Pack canvas and scrollbar + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Display macros + row = 0 + for macro_id, macro in self.macros.items(): + frame = ttk.Frame(scrollable_frame) + frame.pack(fill="x", pady=5, padx=5) + + # Create button with image if available + button_text = macro["name"] + button = tk.Button( + frame, text=button_text, bg=self.button_bg, fg=self.button_fg, + activebackground=self.accent_color, activeforeground=self.button_fg, + relief=tk.RAISED, bd=2, pady=8, command=lambda mid=macro_id: self.execute_macro(mid) + ) + + # Try to display image on button + if "image_path" in macro and macro["image_path"]: + try: + if macro["image_path"] in self.image_cache: + button_image = self.image_cache[macro["image_path"]] + else: + img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"]) + img = Image.open(img_path) + img = img.resize((32, 32)) + button_image = ImageTk.PhotoImage(img) + self.image_cache[macro["image_path"]] = button_image + + button.config(image=button_image, compound=tk.LEFT) + button.image = button_image # Keep a reference + except Exception as e: + print(f"Error loading image for {macro['name']}: {e}") + + button.pack(side=tk.LEFT, fill=tk.X, expand=True) + row += 1 + + # Display message if no macros + if not self.macros: + label = tk.Label(scrollable_frame, text="No macros defined", + bg=self.bg_color, fg=self.fg_color) + label.pack(pady=20) + + def toggle_server(self): + if self.server_running: + self.stop_server() + else: + self.start_server() + + def start_server(self): + try: + if not self.server_running: + self.server_running = True + self.flask_thread = threading.Thread(target=self.run_web_server) + self.flask_thread.daemon = True + self.flask_thread.start() + self.server_button.config(text="Stop Web Server") + + # Get all IP addresses to display + ip_addresses = self.get_ip_addresses() + if ip_addresses: + # Set the URL display + urls = [f"http://{ip}:{self.server_port}" for ip in ip_addresses] + url_text = "Web UI available at:\n" + "\n".join(urls) + self.url_var.set(url_text) + + # Enable browser button + self.browser_button.config(state=tk.NORMAL) + + # Generate and display QR code for the first IP + self.generate_qr_code(urls[0]) + else: + self.url_var.set("No network interfaces found") + except Exception as e: + self.status_var.set(f"Error starting server: {e}") + self.server_running = False + + def stop_server(self): + if self.server_running: + self.server_running = False + self.status_var.set("Web server stopped") + self.server_button.config(text="Start Web Server") + self.url_var.set("") + self.browser_button.config(state=tk.DISABLED) + + # Clear QR code + self.qr_label.config(image="") + + # The Flask server will be stopped on the next request + + def get_ip_addresses(self): + ip_addresses = [] + try: + # Get all network interfaces + interfaces = netifaces.interfaces() + for interface in interfaces: + # Skip loopback interface + if interface.startswith('lo'): + continue + + # Get addresses for this interface + addresses = netifaces.ifaddresses(interface) + if netifaces.AF_INET in addresses: + for address in addresses[netifaces.AF_INET]: + ip = address.get('addr') + if ip and not ip.startswith('127.'): + ip_addresses.append(ip) + + # Always include localhost + ip_addresses.append('localhost') + + return ip_addresses + except Exception as e: + print(f"Error getting IP addresses: {e}") + return ['localhost'] + + def generate_qr_code(self, url): + try: + # Try to import qrcode + import qrcode + + # Generate QR code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(url) + qr.make(fit=True) + + # Create an image from the QR code + qr_img = qr.make_image(fill_color="black", back_color="white") + + # Convert to PhotoImage for display + qr_photoimg = ImageTk.PhotoImage(qr_img) + + # Update label + self.qr_label.config(image=qr_photoimg) + self.qr_label.image = qr_photoimg # Keep a reference + except ImportError: + self.qr_label.config(text="QR code generation requires 'qrcode' package") + except Exception as e: + print(f"Error generating QR code: {e}") + self.qr_label.config(text="Error generating QR code") + + def open_in_browser(self): + # Open the web interface in the default browser + if self.server_running: + webbrowser.open(f"http://localhost:{self.server_port}") + + def create_web_app(self): + app = Flask(__name__) + + # Disable Flask's logging except for errors + log = logging.getLogger('werkzeug') + log.setLevel(logging.ERROR) + + # Define HTML templates + index_html = ''' + + + + + + MacroPad Web Interface + + + +

MacroPad Web Interface

+ +
Macro executed successfully!
+
Failed to execute macro
+ +
+ +
+ + + + + ''' + + @app.route('/') + def index(): + return render_template_string(index_html) + + @app.route('/api/macros') + def get_macros(): + return jsonify(self.macros) + + @app.route('/api/image/') + def get_image(image_path): + try: + # Using os.path.join would translate slashes incorrectly on Windows + # Use normpath instead + image_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), image_path)) + return send_file(image_path) + except Exception as e: + return str(e), 404 + + @app.route('/api/execute', methods=['POST']) + def execute_macro(): + if not self.server_running: + return jsonify({"success": False, "error": "Server is shutting down"}) + + data = request.get_json() + if not data or 'macro_id' not in data: + return jsonify({"success": False, "error": "Invalid request"}) + + macro_id = data['macro_id'] + success = self.execute_macro(macro_id) + return jsonify({"success": success}) + + return app + + def run_web_server(self): + # Create Flask app + self.web_app = self.create_web_app() + + # Update the status in the GUI + self.status_var.set(f"Web server running on port {self.server_port}") + + # Run the Flask app with waitress for production-ready serving + try: + serve(self.web_app, host='0.0.0.0', port=self.server_port, threads=4) + except Exception as e: + self.status_var.set(f"Web server error: {e}") + self.server_running = False + + def add_macro(self): + 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) + + # 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") + 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_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() + macro_id = str(uuid.uuid4()) + + # Process image if provided + 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"]) + 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 create_tray_icon(self): + # Create tray icon + icon_image = Image.new("RGB", (64, 64), self.accent_color) + menu = ( + pystray.MenuItem('Show', self.show_window), + pystray.MenuItem('Exit', self.exit_app) + ) + self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu) + self.tray_icon.run_detached() + + def show_window(self, icon=None, item=None): + # Show the window from tray + self.root.deiconify() + self.root.state('normal') + self.root.lift() + self.root.focus_force() + + def exit_app(self, icon=None, item=None): + # Actually exit the app + self.server_running = False + if hasattr(self, 'tray_icon'): + self.tray_icon.stop() + self.root.quit() + + def minimize_to_tray(self): + # Create tray icon if it doesn't exist + if not hasattr(self, 'tray_icon'): + self.create_tray_icon() + self.root.withdraw() + + 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() \ No newline at end of file diff --git a/mp-server-v2.spec b/mp-server-v2.spec new file mode 100644 index 0000000..ffcefa3 --- /dev/null +++ b/mp-server-v2.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['mp-server-v2.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='mp-server-v2', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/requirements.txt b/requirements.txt index dd465c4..c028438 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ pillow pyautogui -pystray \ No newline at end of file +pystray +flask +waitress +netifaces \ No newline at end of file