Fix PyInstaller build issues and add right-click edit

## 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 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 17:37:43 -08:00
parent f61df830ca
commit a71c1f5ec4
7 changed files with 38 additions and 10 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -43,6 +43,7 @@ a = Analysis(
'qrcode',
'PIL',
'pyautogui',
'pyperclip',
'pystray',
'netifaces',
'websockets',

View File

@@ -46,6 +46,7 @@ a = Analysis(
'qrcode',
'PIL',
'pyautogui',
'pyperclip',
'pystray',
'pystray._base',
'netifaces',

View File

@@ -43,6 +43,7 @@ a = Analysis(
'qrcode',
'PIL',
'pyautogui',
'pyperclip',
'pystray',
'netifaces',
'websockets',

View File

@@ -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

View File

@@ -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")