From a71c1f5ec452657194103e435873e9ef1464cf75 Mon Sep 17 00:00:00 2001 From: jknapp Date: Sat, 3 Jan 2026 17:37:43 -0800 Subject: [PATCH] Fix PyInstaller build issues and add right-click edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Fixes - Web interface now loads correctly in built app (use sys._MEIPASS for bundled web files) - Macro execution no longer locks up (use pyperclip clipboard for Unicode text support) - Right-click context menu works (use Qt signals instead of fragile parent traversal) ## Changes - web_server.py: Use get_resource_path() for web directory - macro_manager.py: Use clipboard paste for text commands instead of typewrite - gui/main_window.py: Add edit_requested/delete_requested signals to MacroButton - pyproject.toml: Add pyperclip dependency - All .spec files: Add pyperclip to hidden imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gui/main_window.py | 10 ++++++++-- macro_manager.py | 18 +++++++++++++----- macropad.spec | 1 + macropad_linux.spec | 1 + macropad_macos.spec | 1 + pyproject.toml | 1 + web_server.py | 16 +++++++++++++--- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/gui/main_window.py b/gui/main_window.py index 04e594b..d0e5936 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -31,6 +31,10 @@ from web_server import WebServer class MacroButton(QPushButton): """Custom button widget for displaying a macro.""" + # Signals for context menu actions + edit_requested = Signal(str) + delete_requested = Signal(str) + def __init__(self, macro_id: str, macro: dict, parent=None): super().__init__(parent) self.macro_id = macro_id @@ -103,9 +107,9 @@ class MacroButton(QPushButton): action = menu.exec_(event.globalPos()) if action == edit_action: - self.parent().parent().parent().parent().edit_macro(self.macro_id) + self.edit_requested.emit(self.macro_id) elif action == delete_action: - self.parent().parent().parent().parent().delete_macro(self.macro_id) + self.delete_requested.emit(self.macro_id) class MainWindow(QMainWindow): @@ -441,6 +445,8 @@ class MainWindow(QMainWindow): for i, (macro_id, macro) in enumerate(filtered): btn = MacroButton(macro_id, macro) btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid)) + btn.edit_requested.connect(self.edit_macro) + btn.delete_requested.connect(self.delete_macro) layout.addWidget(btn, i // cols, i % cols) def on_tab_changed(self, index): diff --git a/macro_manager.py b/macro_manager.py index b2ee1d7..cfda352 100644 --- a/macro_manager.py +++ b/macro_manager.py @@ -275,13 +275,21 @@ class MacroManager: cmd_type = cmd.get("type", "") if cmd_type == "text": - # Type text string + # Type text string using clipboard for Unicode support value = cmd.get("value", "") if value: - if len(value) == 1: - pyautogui.press(value) - else: - pyautogui.typewrite(value, interval=0.02) + try: + import pyperclip + # Save current clipboard, paste text, restore (optional) + pyperclip.copy(value) + pyautogui.hotkey("ctrl", "v") + time.sleep(0.05) # Small delay after paste + except ImportError: + # Fallback to typewrite for ASCII-only + if len(value) == 1: + pyautogui.press(value) + else: + pyautogui.typewrite(value, interval=0.02) elif cmd_type == "key": # Press a single key diff --git a/macropad.spec b/macropad.spec index 0171001..52c2283 100644 --- a/macropad.spec +++ b/macropad.spec @@ -43,6 +43,7 @@ a = Analysis( 'qrcode', 'PIL', 'pyautogui', + 'pyperclip', 'pystray', 'netifaces', 'websockets', diff --git a/macropad_linux.spec b/macropad_linux.spec index aa576ac..b524543 100644 --- a/macropad_linux.spec +++ b/macropad_linux.spec @@ -46,6 +46,7 @@ a = Analysis( 'qrcode', 'PIL', 'pyautogui', + 'pyperclip', 'pystray', 'pystray._base', 'netifaces', diff --git a/macropad_macos.spec b/macropad_macos.spec index 3e7f58a..ae22db6 100644 --- a/macropad_macos.spec +++ b/macropad_macos.spec @@ -43,6 +43,7 @@ a = Analysis( 'qrcode', 'PIL', 'pyautogui', + 'pyperclip', 'pystray', 'netifaces', 'websockets', diff --git a/pyproject.toml b/pyproject.toml index 8a46249..ace7108 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "pillow>=10.0.0", # Keyboard/mouse automation "pyautogui>=0.9.54", + "pyperclip>=1.8.0", # System tray "pystray>=0.19.5", # Web server diff --git a/web_server.py b/web_server.py index 7d42817..d20bf33 100644 --- a/web_server.py +++ b/web_server.py @@ -1,6 +1,7 @@ # FastAPI web server for MacroPad import os +import sys import asyncio from typing import List, Optional from contextlib import asynccontextmanager @@ -14,6 +15,15 @@ import uvicorn from config import DEFAULT_PORT, VERSION +def get_resource_path(relative_path): + """Get the path to a bundled resource file.""" + if getattr(sys, 'frozen', False): + base_path = sys._MEIPASS + else: + base_path = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(base_path, relative_path) + + class Command(BaseModel): """Single command in a macro sequence.""" type: str # text, key, hotkey, wait, app @@ -94,12 +104,12 @@ class WebServer: lifespan=lifespan ) - # Serve static files from web directory - web_dir = os.path.join(self.app_dir, "web") + # Serve static files from web directory (bundled with app) + web_dir = get_resource_path("web") if os.path.exists(web_dir): app.mount("/static", StaticFiles(directory=web_dir), name="static") - # Serve macro images + # Serve macro images (user data directory) images_dir = os.path.join(self.app_dir, "macro_images") if os.path.exists(images_dir): app.mount("/images", StaticFiles(directory=images_dir), name="images")