Big Update

This update will need to get cleaned up, but for right now we are doing testing.
This commit is contained in:
Josh Knapp 2025-06-01 12:38:41 -07:00
parent ddd596e053
commit 9f3484dff2
13 changed files with 3741 additions and 685 deletions

19
config.py Normal file
View File

@ -0,0 +1,19 @@
# Configuration and constants for MacroPad Server
VERSION = "0.5.4 Beta"
DEFAULT_PORT = 40000
# UI Theme colors
THEME = {
'bg_color': "#2e2e2e",
'fg_color': "#ffffff",
'highlight_color': "#3e3e3e",
'accent_color': "#007acc",
'button_bg': "#505050",
'button_fg': "#ffffff",
'tab_bg': "#404040",
'tab_selected': "#007acc"
}
# File extensions for images
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp"]

BIN
dist/mp-server.exe vendored

Binary file not shown.

244
macro_manager.py Normal file
View File

@ -0,0 +1,244 @@
# Macro management and execution
import json
import os
import uuid
import pyautogui
import subprocess
import time
from PIL import Image
class MacroManager:
def __init__(self, data_file, images_dir, app_dir):
self.data_file = data_file
self.images_dir = images_dir
self.app_dir = app_dir
self.macros = {}
self.load_macros()
def load_macros(self):
"""Load macros from JSON file"""
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):
"""Save macros to JSON file"""
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 get_sorted_macros(self, sort_by="name"):
"""Get macros sorted by specified criteria"""
macro_list = list(self.macros.items())
if sort_by == "name":
macro_list.sort(key=lambda x: x[1]["name"].lower())
elif sort_by == "type":
macro_list.sort(key=lambda x: (x[1].get("type", ""), x[1]["name"].lower()))
elif sort_by == "recent":
# Sort by last_used timestamp if available, otherwise by name
macro_list.sort(key=lambda x: (x[1].get("last_used", 0), x[1]["name"].lower()), reverse=True)
return macro_list
def filter_macros_by_tab(self, macro_list, tab_name):
"""Filter macros based on tab name"""
if tab_name == "All":
return macro_list
filtered = []
for macro_id, macro in macro_list:
# Check type match
if macro.get("type", "").title() == tab_name:
filtered.append((macro_id, macro))
# Check custom category match
elif macro.get("category") == tab_name:
filtered.append((macro_id, macro))
return filtered
def get_unique_tabs(self):
"""Get list of unique tabs based on macro types and categories"""
tabs = ["All"]
unique_types = set()
for macro in self.macros.values():
if macro.get("type"):
unique_types.add(macro["type"].title())
if macro.get("category"):
unique_types.add(macro["category"])
for tab_type in sorted(unique_types):
if tab_type not in ["All"]:
tabs.append(tab_type)
return tabs
def add_macro(self, name, macro_type, command, category="", modifiers=None, image_path=""):
"""Add a new macro"""
if modifiers is None:
modifiers = {"ctrl": False, "alt": False, "shift": False, "enter": False}
macro_id = str(uuid.uuid4())
# Process image if provided
image_path_reference = ""
if image_path:
try:
# Generate unique filename for the image
file_ext = os.path.splitext(image_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(image_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 data
macro_data = {
"name": name,
"type": macro_type,
"command": command,
"image_path": image_path_reference,
"modifiers": modifiers,
"last_used": 0
}
if category:
macro_data["category"] = category
self.macros[macro_id] = macro_data
self.save_macros()
return macro_id
def update_macro(self, macro_id, name, macro_type, command, category="", modifiers=None, image_path=""):
"""Update an existing macro"""
if macro_id not in self.macros:
return False
if modifiers is None:
modifiers = {"ctrl": False, "alt": False, "shift": False, "enter": False}
macro = self.macros[macro_id]
# Keep the old image or update with new one
image_path_reference = macro.get("image_path", "")
if image_path:
try:
# Generate unique filename for the image
file_ext = os.path.splitext(image_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(image_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 data
updated_macro = {
"name": name,
"type": macro_type,
"command": command,
"image_path": image_path_reference,
"modifiers": modifiers,
"last_used": macro.get("last_used", 0)
}
if category:
updated_macro["category"] = category
self.macros[macro_id] = updated_macro
self.save_macros()
return True
def delete_macro(self, macro_id):
"""Delete a macro"""
if macro_id not in self.macros:
return False
macro = self.macros[macro_id]
# Delete associated image file if it exists
if "image_path" in macro and macro["image_path"]:
try:
img_path = os.path.join(self.app_dir, 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[macro_id]
self.save_macros()
return True
def execute_macro(self, macro_id):
"""Execute a macro by ID"""
if macro_id not in self.macros:
return False
macro = self.macros[macro_id]
# Update last_used timestamp for recent sorting
self.macros[macro_id]["last_used"] = time.time()
self.save_macros()
try:
if macro["type"] == "text":
# Handle key modifiers
modifiers = macro.get("modifiers", {})
# Add modifier keys if enabled
if modifiers.get("ctrl", False):
pyautogui.keyDown('ctrl')
if modifiers.get("alt", False):
pyautogui.keyDown('alt')
if modifiers.get("shift", False):
pyautogui.keyDown('shift')
# Handle single character vs multi-character commands
if str(macro["command"]) and len(str(macro["command"])) == 1:
pyautogui.keyDown(macro["command"])
time.sleep(0.5)
pyautogui.keyUp(macro["command"])
else:
pyautogui.typewrite(macro["command"], interval=0.02)
# Release modifier keys in reverse order
if modifiers.get("shift", False):
pyautogui.keyUp('shift')
if modifiers.get("alt", False):
pyautogui.keyUp('alt')
if modifiers.get("ctrl", False):
pyautogui.keyUp('ctrl')
# 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

442
main.py Normal file
View File

@ -0,0 +1,442 @@
# Main application file for MacroPad Server
import tkinter as tk
from tkinter import ttk, messagebox
import os
import sys
import threading
import socket
import qrcode
import webbrowser
import pystray
from PIL import Image, ImageTk
from config import VERSION, DEFAULT_PORT, THEME
from macro_manager import MacroManager
from web_server import WebServer
from ui_components import MacroDialog, MacroSelector, TabManager
class MacroPadServer:
def __init__(self, root):
self.root = root
self.root.title("MacroPad Server")
self.root.geometry("800x600")
self.configure_styles()
# Set up directories
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
data_file = os.path.join(base_dir, "macros.json")
images_dir = os.path.join(base_dir, "macro_images")
os.makedirs(images_dir, exist_ok=True)
# Initialize components
self.macro_manager = MacroManager(data_file, images_dir, base_dir)
self.web_server = WebServer(self.macro_manager, base_dir, DEFAULT_PORT)
# UI state
self.current_sort = "name"
self.current_tab = "All"
self.image_cache = {}
# Server state
self.server_running = False
self.flask_thread = None
# Create UI
self.create_ui()
# Set up window close handler
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def configure_styles(self):
"""Configure the dark theme styles"""
self.root.configure(bg=THEME['bg_color'])
style = ttk.Style()
style.theme_use("clam")
style.configure("TButton", background=THEME['button_bg'], foreground=THEME['button_fg'])
style.map("TButton", background=[("active", THEME['accent_color'])])
style.configure("TFrame", background=THEME['bg_color'])
style.configure("TLabel", background=THEME['bg_color'], foreground=THEME['fg_color'])
# Configure notebook (tabs) style
style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0)
style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'],
padding=[12, 8], borderwidth=0)
style.map("TNotebook.Tab", background=[("selected", THEME['tab_selected'])],
foreground=[("selected", THEME['fg_color'])])
def create_ui(self):
"""Create the main user interface"""
# 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 controls
left_frame = ttk.Frame(main_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Sort controls
self._create_sort_controls(left_frame)
# Create notebook for tabs
self.notebook = ttk.Notebook(left_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
# Button controls
self._create_macro_buttons(left_frame)
# Right side: Server controls
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
self._create_server_controls(right_frame)
# Version label
version_label = tk.Label(self.root, text=VERSION,
bg=THEME['bg_color'], fg=THEME['fg_color'],
font=('Helvetica', 8))
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
# Initialize display
self.setup_tabs()
self.display_macros()
def _create_sort_controls(self, parent):
"""Create sorting controls"""
sort_frame = ttk.Frame(parent)
sort_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(sort_frame, text="Sort by:").pack(side=tk.LEFT, padx=(0, 5))
self.sort_var = tk.StringVar(value=self.current_sort)
sort_combo = ttk.Combobox(sort_frame, textvariable=self.sort_var,
values=["name", "type", "recent"],
state="readonly", width=10)
sort_combo.pack(side=tk.LEFT, padx=(0, 10))
sort_combo.bind("<<ComboboxSelected>>", self.on_sort_change)
# Tab management button
ttk.Button(sort_frame, text="Manage Tabs",
command=self.manage_tabs).pack(side=tk.RIGHT, padx=(5, 0))
def _create_macro_buttons(self, parent):
"""Create macro management buttons"""
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(button_frame, text="Add Macro", command=self.add_macro).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Edit Macro", command=self.edit_macro).pack(side=tk.LEFT, padx=2)
ttk.Button(button_frame, text="Delete Macro", command=self.delete_macro).pack(side=tk.LEFT, padx=2)
def _create_server_controls(self, parent):
"""Create web server controls"""
server_frame = ttk.Frame(parent)
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 display
self.qr_label = ttk.Label(parent)
self.qr_label.pack(pady=10)
# URL display
self.url_var = tk.StringVar(value="")
self.url_label = ttk.Label(parent, textvariable=self.url_var)
self.url_label.pack(pady=5)
# Browser button
self.browser_button = ttk.Button(parent, text="Open in Browser",
command=self.open_in_browser, state=tk.DISABLED)
self.browser_button.pack(pady=5)
def setup_tabs(self):
"""Initialize tabs based on macro categories"""
# Clear existing tabs
for tab in self.notebook.tabs():
self.notebook.forget(tab)
# Get unique tabs from macro manager
tabs = self.macro_manager.get_unique_tabs()
for tab_name in tabs:
frame = ttk.Frame(self.notebook)
self.notebook.add(frame, text=tab_name)
def get_current_tab_name(self):
"""Get the name of the currently selected tab"""
try:
current_tab_id = self.notebook.select()
return self.notebook.tab(current_tab_id, "text")
except:
return "All"
def on_tab_change(self, event=None):
"""Handle tab change event"""
self.current_tab = self.get_current_tab_name()
self.display_macros()
def on_sort_change(self, event=None):
"""Handle sort option change"""
self.current_sort = self.sort_var.get()
self.display_macros()
def display_macros(self):
"""Display macros in the current tab"""
# Get current tab frame
try:
current_tab_id = self.notebook.select()
current_frame = self.notebook.nametowidget(current_tab_id)
except:
return
# Clear previous content
for widget in current_frame.winfo_children():
widget.destroy()
# Create scrollable canvas
canvas = tk.Canvas(current_frame, bg=THEME['bg_color'], highlightthickness=0)
scrollbar = ttk.Scrollbar(current_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Get sorted and filtered macros
sorted_macros = self.macro_manager.get_sorted_macros(self.current_sort)
filtered_macros = self.macro_manager.filter_macros_by_tab(sorted_macros, self.current_tab)
# Display macros
for macro_id, macro in filtered_macros:
self._create_macro_button(scrollable_frame, macro_id, macro)
# Display message if no macros
if not filtered_macros:
label = tk.Label(scrollable_frame, text="No macros in this category",
bg=THEME['bg_color'], fg=THEME['fg_color'])
label.pack(pady=20)
def _create_macro_button(self, parent, macro_id, macro):
"""Create a button for a single macro"""
frame = ttk.Frame(parent)
frame.pack(fill="x", pady=5, padx=5)
button = tk.Button(
frame, text=macro["name"],
bg=THEME['button_bg'], fg=THEME['button_fg'],
activebackground=THEME['accent_color'], activeforeground=THEME['button_fg'],
relief=tk.RAISED, bd=2, pady=8,
command=lambda: self.macro_manager.execute_macro(macro_id)
)
# Add image if available
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(self.macro_manager.app_dir, 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 reference
except Exception as e:
print(f"Error loading image for {macro['name']}: {e}")
button.pack(side=tk.LEFT, fill=tk.X, expand=True)
def manage_tabs(self):
"""Open tab management dialog"""
tab_manager = TabManager(self.root, self.macro_manager)
tab_manager.show()
self.setup_tabs()
self.display_macros()
def add_macro(self):
"""Add a new macro"""
dialog = MacroDialog(self.root, self.macro_manager)
result = dialog.show()
if result:
self.setup_tabs()
self.display_macros()
def edit_macro(self):
"""Edit an existing macro"""
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Edit")
macro_id = selector.show()
if macro_id:
dialog = MacroDialog(self.root, self.macro_manager, macro_id)
result = dialog.show()
if result:
self.setup_tabs()
self.display_macros()
def delete_macro(self):
"""Delete a macro"""
selector = MacroSelector(self.root, self.macro_manager, "Select Macro to Delete")
macro_id = selector.show()
if macro_id:
macro_name = self.macro_manager.macros[macro_id]["name"]
if messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete macro '{macro_name}'?"):
self.macro_manager.delete_macro(macro_id)
self.setup_tabs()
self.display_macros()
def toggle_server(self):
"""Toggle web server on/off"""
if self.server_running:
self.stop_server()
else:
self.start_server()
def start_server(self):
"""Start the web server"""
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 IP address and display info
ip_address = self.get_ip_address()
if ip_address:
url = f"http://{ip_address}:{DEFAULT_PORT}"
url_text = f"Web UI available at:\n{url}"
self.url_var.set(url_text)
self.browser_button.config(state=tk.NORMAL)
self.generate_qr_code(url)
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):
"""Stop the web server"""
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)
self.qr_label.config(image="")
def run_web_server(self):
"""Run the web server in a separate thread"""
self.status_var.set(f"Web server running on port {DEFAULT_PORT}")
try:
self.web_server.run()
except Exception as e:
self.status_var.set(f"Web server error: {e}")
self.server_running = False
def get_ip_address(self):
"""Get the primary internal IPv4 address"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
print(f"Error getting IP address: {e}")
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
if not ip.startswith("127."):
return ip
for addr_info in socket.getaddrinfo(hostname, None):
potential_ip = addr_info[4][0]
if '.' in potential_ip and not potential_ip.startswith("127."):
return potential_ip
except:
pass
return "127.0.0.1"
def generate_qr_code(self, url):
"""Generate and display QR code for the URL"""
try:
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)
qr_img = qr.make_image(fill_color="black", back_color="white")
qr_photoimg = ImageTk.PhotoImage(qr_img)
self.qr_label.config(image=qr_photoimg)
self.qr_label.image = qr_photoimg
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 browser"""
if self.server_running:
webbrowser.open(f"http://localhost:{DEFAULT_PORT}")
def create_tray_icon(self):
"""Create system tray icon"""
icon_image = Image.new("RGB", (64, 64), THEME['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 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):
"""Exit the application"""
self.stop_server()
if hasattr(self, 'tray_icon'):
self.tray_icon.stop()
self.root.quit()
def on_closing(self):
"""Handle window close event"""
# For now just exit, but could minimize to tray
self.exit_app()
if __name__ == "__main__":
root = tk.Tk()
app = MacroPadServer(root)
root.mainloop()

1183
mp-server-v2-enhanced.py Normal file

File diff suppressed because it is too large Load Diff

1176
mp-server-v2-new.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,645 +0,0 @@
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("<Unmap>", 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(
"<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, 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()

View File

@ -1,38 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['mp-server.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',
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,
)

View File

@ -5,3 +5,4 @@ flask
waitress
netifaces
qrcode
tkinter

283
ui_components.py Normal file
View File

@ -0,0 +1,283 @@
# UI components and dialogs for the desktop application
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import os
import uuid
from config import THEME
class MacroDialog:
"""Dialog for adding/editing macros"""
def __init__(self, parent, macro_manager, macro_id=None):
self.parent = parent
self.macro_manager = macro_manager
self.macro_id = macro_id
self.dialog = None
self.result = None
def show(self):
"""Show the dialog and return the result"""
self.dialog = tk.Toplevel(self.parent)
self.dialog.title("Edit Macro" if self.macro_id else "Add Macro")
self.dialog.geometry("450x400")
self.dialog.transient(self.parent)
self.dialog.configure(bg=THEME['bg_color'])
self.dialog.grab_set()
# If editing, get existing macro data
if self.macro_id:
macro = self.macro_manager.macros.get(self.macro_id, {})
else:
macro = {}
self._create_widgets(macro)
# Wait for dialog to close
self.dialog.wait_window()
return self.result
def _create_widgets(self, macro):
"""Create the dialog widgets"""
# Name
tk.Label(self.dialog, text="Macro Name:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.name_entry = tk.Entry(self.dialog, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
self.name_entry.grid(row=0, column=1, padx=5, pady=5)
self.name_entry.insert(0, macro.get("name", ""))
# Category
tk.Label(self.dialog, text="Category:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.category_entry = tk.Entry(self.dialog, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
self.category_entry.grid(row=1, column=1, padx=5, pady=5)
self.category_entry.insert(0, macro.get("category", ""))
# Type
tk.Label(self.dialog, text="Type:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.type_var = tk.StringVar(value=macro.get("type", "text"))
radio_style = {'bg': THEME['bg_color'], 'fg': THEME['fg_color'], 'selectcolor': THEME['accent_color']}
tk.Radiobutton(self.dialog, text="Text", variable=self.type_var, value="text", **radio_style).grid(row=2, column=1, sticky="w")
tk.Radiobutton(self.dialog, text="Application", variable=self.type_var, value="app", **radio_style).grid(row=3, column=1, sticky="w")
# Command/Text
tk.Label(self.dialog, text="Command/Text:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=4, column=0, padx=5, pady=5, sticky="w")
self.command_text = tk.Text(self.dialog, width=30, height=5, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
self.command_text.grid(row=4, column=1, padx=5, pady=5)
self.command_text.insert("1.0", macro.get("command", ""))
# Modifiers
self._create_modifiers(macro)
# Image
self.image_path = tk.StringVar()
tk.Label(self.dialog, text="Image:", bg=THEME['bg_color'], fg=THEME['fg_color']).grid(row=6, column=0, padx=5, pady=5, sticky="w")
image_entry = tk.Entry(self.dialog, textvariable=self.image_path, width=30, bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
image_entry.grid(row=6, column=1, padx=5, pady=5)
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
tk.Button(self.dialog, text="Browse...", command=self._browse_image, **button_style).grid(row=6, column=2)
# Buttons
tk.Button(self.dialog, text="Save", command=self._save, **button_style).grid(row=7, column=0, padx=5, pady=20)
tk.Button(self.dialog, text="Cancel", command=self._cancel, **button_style).grid(row=7, column=1, padx=5, pady=20)
def _create_modifiers(self, macro):
"""Create modifier checkboxes"""
mod_frame = tk.Frame(self.dialog, bg=THEME['bg_color'])
mod_frame.grid(row=5, column=0, columnspan=2, sticky="w", padx=5, pady=5)
tk.Label(mod_frame, text="Key Modifiers:", bg=THEME['bg_color'], fg=THEME['fg_color']).pack(side=tk.LEFT, padx=5)
modifiers = macro.get("modifiers", {})
self.ctrl_var = tk.BooleanVar(value=modifiers.get("ctrl", False))
self.alt_var = tk.BooleanVar(value=modifiers.get("alt", False))
self.shift_var = tk.BooleanVar(value=modifiers.get("shift", False))
self.enter_var = tk.BooleanVar(value=modifiers.get("enter", False))
checkbox_style = {'bg': THEME['bg_color'], 'fg': THEME['fg_color'], 'selectcolor': THEME['accent_color'],
'activebackground': THEME['bg_color'], 'activeforeground': THEME['fg_color']}
tk.Checkbutton(mod_frame, text="Ctrl", variable=self.ctrl_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
tk.Checkbutton(mod_frame, text="Alt", variable=self.alt_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
tk.Checkbutton(mod_frame, text="Shift", variable=self.shift_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
tk.Checkbutton(mod_frame, text="Add Enter", variable=self.enter_var, **checkbox_style).pack(side=tk.LEFT, padx=5)
def _browse_image(self):
"""Browse for image file"""
filename = filedialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")])
if filename:
self.image_path.set(filename)
def _save(self):
"""Save the macro"""
name = self.name_entry.get().strip()
if not name:
messagebox.showerror("Error", "Macro name is required")
return
macro_type = self.type_var.get()
command = self.command_text.get("1.0", tk.END).strip()
category = self.category_entry.get().strip()
modifiers = {
"ctrl": self.ctrl_var.get(),
"alt": self.alt_var.get(),
"shift": self.shift_var.get(),
"enter": self.enter_var.get()
}
if self.macro_id:
# Update existing macro
success = self.macro_manager.update_macro(
self.macro_id, name, macro_type, command, category, modifiers, self.image_path.get())
else:
# Add new macro
self.macro_id = self.macro_manager.add_macro(
name, macro_type, command, category, modifiers, self.image_path.get())
success = bool(self.macro_id)
if success:
self.result = self.macro_id
self.dialog.destroy()
else:
messagebox.showerror("Error", "Failed to save macro")
def _cancel(self):
"""Cancel dialog"""
self.result = None
self.dialog.destroy()
class MacroSelector:
"""Dialog for selecting a macro from a list"""
def __init__(self, parent, macro_manager, title="Select Macro"):
self.parent = parent
self.macro_manager = macro_manager
self.title = title
self.result = None
def show(self):
"""Show the selection dialog"""
if not self.macro_manager.macros:
messagebox.showinfo("No Macros", "There are no macros available.")
return None
dialog = tk.Toplevel(self.parent)
dialog.title(self.title)
dialog.geometry("200x340")
dialog.transient(self.parent)
dialog.configure(bg=THEME['bg_color'])
dialog.grab_set()
# Instructions
tk.Label(dialog, text=f"{self.title}:", bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=5)
# Listbox
listbox = tk.Listbox(dialog, bg=THEME['highlight_color'], fg=THEME['fg_color'], selectbackground=THEME['accent_color'])
listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Populate listbox
macro_ids = []
for macro_id, macro in self.macro_manager.macros.items():
listbox.insert(tk.END, macro["name"])
macro_ids.append(macro_id)
def on_select():
if not listbox.curselection():
messagebox.showwarning("No Selection", f"Please select a macro.")
return
idx = listbox.curselection()[0]
self.result = macro_ids[idx]
dialog.destroy()
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
tk.Button(dialog, text="Select", command=on_select, **button_style).pack(pady=10)
tk.Button(dialog, text="Cancel", command=lambda: dialog.destroy(), **button_style).pack(pady=5)
dialog.wait_window()
return self.result
class TabManager:
"""Dialog for managing macro categories/tabs"""
def __init__(self, parent, macro_manager):
self.parent = parent
self.macro_manager = macro_manager
def show(self):
"""Show tab management dialog"""
dialog = tk.Toplevel(self.parent)
dialog.title("Manage Tabs")
dialog.geometry("400x300")
dialog.transient(self.parent)
dialog.configure(bg=THEME['bg_color'])
dialog.grab_set()
# Instructions
tk.Label(dialog, text="Assign categories to macros for better organization:",
bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=10)
# Create scrollable frame
list_frame = ttk.Frame(dialog)
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
canvas = tk.Canvas(list_frame, bg=THEME['bg_color'], highlightthickness=0)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Category entries for each macro
category_vars = {}
for macro_id, macro in self.macro_manager.macros.items():
frame = ttk.Frame(scrollable_frame)
frame.pack(fill="x", pady=2, padx=5)
tk.Label(frame, text=macro["name"], bg=THEME['bg_color'], fg=THEME['fg_color'],
width=20, anchor="w").pack(side=tk.LEFT)
category_var = tk.StringVar(value=macro.get("category", ""))
category_vars[macro_id] = category_var
entry = tk.Entry(frame, textvariable=category_var, width=15,
bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
entry.pack(side=tk.RIGHT, padx=(5, 0))
# Buttons
button_frame = ttk.Frame(dialog)
button_frame.pack(fill=tk.X, pady=10)
def save_categories():
for macro_id, category_var in category_vars.items():
category = category_var.get().strip()
if category:
self.macro_manager.macros[macro_id]["category"] = category
else:
self.macro_manager.macros[macro_id].pop("category", None)
self.macro_manager.save_macros()
dialog.destroy()
button_style = {'bg': THEME['button_bg'], 'fg': THEME['button_fg'], 'activebackground': THEME['accent_color'],
'activeforeground': THEME['button_fg'], 'bd': 0, 'relief': tk.FLAT}
tk.Button(button_frame, text="Save", command=save_categories, **button_style).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="Cancel", command=lambda: dialog.destroy(), **button_style).pack(side=tk.LEFT, padx=5)
dialog.wait_window()

99
web_server.py Normal file
View File

@ -0,0 +1,99 @@
# Web server component for MacroPad
from flask import Flask, render_template_string, request, jsonify, send_file
from waitress import serve
import logging
import os
from web_templates import INDEX_HTML
class WebServer:
def __init__(self, macro_manager, app_dir, port=40000):
self.macro_manager = macro_manager
self.app_dir = app_dir
self.port = port
self.app = None
def create_app(self):
"""Create and configure Flask application"""
app = Flask(__name__)
# Disable Flask's logging except for errors
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
@app.route('/')
def index():
return render_template_string(INDEX_HTML)
@app.route('/api/tabs')
def get_tabs():
"""Get all available tabs (similar to setup_tabs logic)"""
tabs = ["All"]
# Add tabs based on macro types and custom categories
unique_types = set()
for macro in self.macro_manager.macros.values():
if macro.get("type"):
unique_types.add(macro["type"].title())
# Check for custom category
if macro.get("category"):
unique_types.add(macro["category"])
for tab_type in sorted(unique_types):
if tab_type not in ["All"]:
tabs.append(tab_type)
return jsonify(tabs)
@app.route('/api/macros')
def get_macros():
return jsonify(self.macro_manager.macros)
@app.route('/api/macros/<tab_name>')
def get_macros_by_tab(tab_name):
"""Filter macros by tab (similar to filter_macros_by_tab logic)"""
if tab_name == "All":
return jsonify(self.macro_manager.macros)
filtered_macros = {}
for macro_id, macro in self.macro_manager.macros.items():
# Check type match
if macro.get("type", "").title() == tab_name:
filtered_macros[macro_id] = macro
# Check custom category match
elif macro.get("category") == tab_name:
filtered_macros[macro_id] = macro
return jsonify(filtered_macros)
@app.route('/api/image/<path:image_path>')
def get_image(image_path):
try:
image_path = os.path.join(self.app_dir, image_path)
return send_file(image_path)
except Exception as e:
return str(e), 404
@app.route('/api/execute', methods=['POST'])
def execute_macro():
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.macro_manager.execute_macro(macro_id)
return jsonify({"success": success})
self.app = app
return app
def run(self):
"""Run the web server"""
if not self.app:
self.create_app()
try:
serve(self.app, host='0.0.0.0', port=self.port, threads=4)
except Exception as e:
raise e

292
web_templates.py Normal file
View File

@ -0,0 +1,292 @@
# HTML templates for the web interface
INDEX_HTML = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MacroPad Web Interface</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #2e2e2e;
color: #ffffff;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h1 {
color: #007acc;
margin: 0;
}
.refresh-button {
background-color: #505050;
color: white;
border: none;
border-radius: 8px;
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
}
.refresh-button:hover {
background-color: #007acc;
}
.refresh-button svg {
margin-right: 5px;
}
.tab-container {
margin-bottom: 20px;
border-bottom: 2px solid #404040;
}
.tab-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 0;
}
.tab-button {
background-color: #404040;
color: #ffffff;
border: none;
border-radius: 8px 8px 0 0;
padding: 10px 15px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
border-bottom: 3px solid transparent;
}
.tab-button:hover {
background-color: #505050;
}
.tab-button.active {
background-color: #007acc;
border-bottom-color: #007acc;
}
.macro-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.macro-button {
background-color: #505050;
color: white;
border: none;
border-radius: 8px;
padding: 15px 10px;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100px;
}
.macro-button:hover, .macro-button:active {
background-color: #007acc;
}
.macro-button img {
max-width: 64px;
max-height: 64px;
margin-bottom: 10px;
}
.status {
text-align: center;
margin: 20px 0;
padding: 10px;
border-radius: 5px;
}
.success {
background-color: #4CAF50;
color: white;
display: none;
}
.error {
background-color: #f44336;
color: white;
display: none;
}
@media (max-width: 600px) {
.macro-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.macro-button {
padding: 10px 5px;
font-size: 14px;
}
h1 {
font-size: 24px;
}
.refresh-button {
padding: 8px 12px;
font-size: 14px;
}
.tab-button {
padding: 8px 12px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div class="header-container">
<h1>MacroPad Web Interface</h1>
<button class="refresh-button" onclick="loadTabs()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
Refresh
</button>
</div>
<div class="status success" id="success-status">Macro executed successfully!</div>
<div class="status error" id="error-status">Failed to execute macro</div>
<div class="tab-container">
<div class="tab-list" id="tab-list">
<!-- Tabs will be loaded here -->
</div>
</div>
<div class="macro-grid" id="macro-grid">
<!-- Macros will be loaded here -->
</div>
<script>
let currentTab = 'All';
let allMacros = {};
document.addEventListener('DOMContentLoaded', function() {
loadTabs();
});
function loadTabs() {
fetch('/api/tabs')
.then(response => response.json())
.then(tabs => {
const tabList = document.getElementById('tab-list');
tabList.innerHTML = '';
tabs.forEach(tab => {
const button = document.createElement('button');
button.className = 'tab-button';
button.textContent = tab;
button.onclick = function() { switchTab(tab); };
if (tab === currentTab) {
button.classList.add('active');
}
tabList.appendChild(button);
});
// Load macros for current tab
loadMacros(currentTab);
})
.catch(error => {
console.error('Error loading tabs:', error);
});
}
function switchTab(tabName) {
currentTab = tabName;
// Update tab button states
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.classList.remove('active');
if (button.textContent === tabName) {
button.classList.add('active');
}
});
// Load macros for selected tab
loadMacros(tabName);
}
function loadMacros(tab = 'All') {
const url = tab === 'All' ? '/api/macros' : `/api/macros/${encodeURIComponent(tab)}`;
fetch(url)
.then(response => response.json())
.then(macros => {
allMacros = macros;
displayMacros(macros);
})
.catch(error => {
console.error('Error loading macros:', error);
});
}
function displayMacros(macros) {
const macroGrid = document.getElementById('macro-grid');
macroGrid.innerHTML = '';
if (Object.keys(macros).length === 0) {
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros in this category.</p>';
return;
}
for (const [macroId, macro] of Object.entries(macros)) {
const button = document.createElement('button');
button.className = 'macro-button';
button.onclick = function() { executeMacro(macroId); };
// Add image if available
if (macro.image_path) {
const img = document.createElement('img');
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
img.alt = macro.name;
img.onerror = function() {
this.style.display = 'none';
};
button.appendChild(img);
}
const text = document.createTextNode(macro.name);
button.appendChild(text);
macroGrid.appendChild(button);
}
}
function executeMacro(macroId) {
fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ macro_id: macroId })
})
.then(response => response.json())
.then(data => {
const successStatus = document.getElementById('success-status');
const errorStatus = document.getElementById('error-status');
if (data.success) {
successStatus.style.display = 'block';
errorStatus.style.display = 'none';
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
} else {
errorStatus.style.display = 'block';
successStatus.style.display = 'none';
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
}
})
.catch(error => {
console.error('Error executing macro:', error);
const errorStatus = document.getElementById('error-status');
errorStatus.style.display = 'block';
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
});
}
</script>
</body>
</html>
'''