8 Commits
Author SHA1 Message Date
jknapp 80f4c30fb9 Merge pull request 'update-build-and-docs' (#2) from update-build-and-docs into main
Build and Release / create-release (push) Successful in 5s
Build and Release / build-windows (push) Failing after 34s
Build and Release / build-linux (push) Failing after 39s
Build and Release / attach-to-release (push) Has been skipped
Reviewed-on: #2
2025-06-05 21:34:40 +00:00
shadowdao c80474f520 update to remove mac build because I do not have a mac to build on yet 2025-06-05 14:33:03 -07:00
shadowdao 0a66c1d663 Enhanced project documentation and release automation
- Improved README.md formatting and structure
- Added installation instructions for pre-built executables
- Included platform-specific examples for Windows, Linux, and macOS
- Set up Gitea workflow for automated builds on new releases
- Added platform-specific PyInstaller spec files for all supported platforms
- Updated repository links to point to repo.anhonesthost.net

These changes make the project more accessible to new users and streamline
the release process with automatic executable generation for all platforms.
2025-06-05 13:54:36 -07:00
jknapp 2f8fa238fc Merge pull request 'Add support for Tabs and other enhancements' (#1) from new-rework into main
Reviewed-on: #1
2025-06-01 21:19:42 +00:00
shadowdao d2a9be6274 Updating README
Updated the README to reflect the changes to the app including the tabs
2025-06-01 14:18:27 -07:00
shadowdao a477be23cc Updating README
Updated the README to reflect the changes to the app including the tabs
2025-06-01 14:17:50 -07:00
shadowdao 5727a9ee68 Release Update
Updated MacroPad to support Tabs, fix system tray behavior, and break apart the monolith
2025-06-01 14:02:29 -07:00
shadowdao 896875ce90 updating main app to resolve previous issues
Tabs are working correctly, and the app that minimizes to the tray
2025-06-01 13:27:27 -07:00
15 changed files with 437 additions and 3380 deletions
+140
View File
@@ -0,0 +1,140 @@
name: Build and Release
on:
push:
branches:
- main
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get version
id: get_version
run: |
VERSION=$(cat version.txt)
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ env.VERSION }}
name: Release v${{ env.VERSION }}
draft: false
prerelease: false
build-windows:
needs: [create-release]
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
pip install -r requirements.txt
- name: Build executable
run: |
pyinstaller macropad.spec
- name: Upload Windows artifact
uses: actions/upload-artifact@v3
with:
name: macropad-windows
path: dist/macropad.exe
build-linux:
needs: [create-release]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
pip install -r requirements.txt
- name: Build executable
run: |
pyinstaller macropad_linux.spec
- name: Upload Linux artifact
uses: actions/upload-artifact@v3
with:
name: macropad-linux
path: dist/macropad
# MacOS build is temporarily disabled
# Uncomment this section when macOS build environment becomes available
#
# build-macos:
# needs: [create-release]
# runs-on: macos-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
#
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: '3.11'
#
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install pyinstaller
# pip install -r requirements.txt
#
# - name: Build executable
# run: |
# pyinstaller macropad_macos.spec
#
# - name: Upload macOS artifact
# uses: actions/upload-artifact@v3
# with:
# name: macropad-macos
# path: dist/macropad.app
attach-to-release:
needs: [create-release, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get version
id: get_version
run: |
VERSION=$(cat version.txt)
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Download all artifacts
uses: actions/download-artifact@v3
- name: Attach executables to release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ env.VERSION }}
files: |
macropad-windows/macropad.exe
macropad-linux/macropad
# macropad-macos/macropad.app/**/*
+3 -1
View File
@@ -1,4 +1,6 @@
macros.json macros.json
macro_images/ macro_images/
macro_images/* macro_images/*
build/ build/
.venv
__pycache__
+86 -22
View File
@@ -1,4 +1,4 @@
# MP-Server # MacroPad Server
A versatile MacroPad server application that lets you create, manage, and execute custom macros from both a local interface and remotely via a web interface. A versatile MacroPad server application that lets you create, manage, and execute custom macros from both a local interface and remotely via a web interface.
@@ -8,61 +8,69 @@ A versatile MacroPad server application that lets you create, manage, and execut
- **Application Macros**: Launch applications or scripts directly - **Application Macros**: Launch applications or scripts directly
- **Key Modifiers**: Add Ctrl, Alt, Shift modifiers and Enter keypress to your text macros - **Key Modifiers**: Add Ctrl, Alt, Shift modifiers and Enter keypress to your text macros
- **Custom Images**: Assign images to macros for easy identification - **Custom Images**: Assign images to macros for easy identification
- **Category Management**: Organize macros into custom tabs for better organization
- **Web Interface**: Access and trigger your macros from other devices on your network - **Web Interface**: Access and trigger your macros from other devices on your network
- **System Tray Integration**: Runs silently in your system tray for easy access - **System Tray Integration**: Minimize to tray when minimized, exit when closed
- **QR Code Generation**: Quickly connect mobile devices to the web interface
- **Sorting Options**: Sort macros by name, type, or recent usage
- **Persistent Storage**: Macros are automatically saved for future sessions - **Persistent Storage**: Macros are automatically saved for future sessions
- **Dark Theme**: Modern dark interface for comfortable use
- **Modular Architecture**: Clean separation of concerns with dedicated modules
## Requirements ## Requirements
- Python 3.11+ - Python 3.11+
- Required Python packages: - Required Python packages (install via requirements.txt):
- tkinter - tkinter
- flask - flask
- pyautogui - pyautogui
- pystray - pystray
- Pillow (PIL) - Pillow (PIL)
- flask_cors
- waitress - waitress
- netifaces
- qrcode
## Installation ## Installation
### Method 1: From Source
1. Clone or download this repository 1. Clone or download this repository
2. Install the required dependencies: 2. Install the required dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. Run the application:
```bash
python mp-server-v2.py
```
## Alternative Installation Method ### Method 2: Pre-built Executables
#### Windows only
1. Create a Folder you wish to run MacroPad from 1. Go to the [Releases](https://repo.anhonesthost.net/MacroPad/MP-Server/releases) page
2. Download ```mp-serverv2.exe``` from the ```dist``` folder 2. Download the appropriate version for your operating system:
3. Accept the security notices, and run the application - Windows: `macropad.exe`
- Linux: `macropad`
- macOS: `macropad.app`
3. Run the downloaded file
> [!IMPORTANT] > [!IMPORTANT]
> The executable is unsigned and may trigger security warnings. You may need to click "More info" and "Run anyway" in Windows SmartScreen or add an exception in your antivirus software. > The executables are unsigned and may trigger security warnings. You may need to click "More info" and "Run anyway" in Windows SmartScreen, adjust permissions on Linux (`chmod +x macropad`), or override Gatekeeper on macOS.
## Usage ## Usage
### Main Interface ### Main Interface
When launched, MP-Server displays your existing macros with options to: When launched, MacroPad displays your existing macros with options to:
- **Add New Macro**: Create text snippets or application shortcuts - **Add New Macro**: Create text snippets or application shortcuts
- **Edit Macro**: Modify existing macros - **Edit Macro**: Modify existing macros
- **Delete Macro**: Remove unwanted macros - **Delete Macro**: Remove unwanted macros
- **Minimize to Tray**: Hide the application to your system tray - **Sort Options**: Sort the Macros by type, name, and recent usage
- **Exit**: Close the application completely - **Manage Tabs**: Assign categories to macros for better organization
- **Start Web Server**: Starts the web server to serve the MacroPad web interface.
### Creating a Macro ### Creating a Macro
1. Click the "Add Macro" button 1. Click the "Add Macro" button
2. Fill in the details: 2. Fill in the details:
- **Name**: A descriptive name for your macro - **Name**: A descriptive name for your macro
- **Category**: Assign a category to associate with a tab
- **Type**: Choose between Text or Application - **Type**: Choose between Text or Application
- **Command/Text**: The text to insert or application command to run - **Command/Text**: The text to insert or application command to run
- **Modifiers**: Select any combination of Ctrl, Alt, Shift, and Enter - **Modifiers**: Select any combination of Ctrl, Alt, Shift, and Enter
@@ -90,11 +98,67 @@ When minimized to the system tray:
### Windows Examples ### Windows Examples
#### Steam Applications #### Steam Applications
```"C:\Program Files (x86)\Steam\steam.exe" steam://rungameid/2767030``` ```
"C:\Program Files (x86)\Steam\steam.exe" steam://rungameid/2767030
```
#### Chrome to a website #### Chrome to a website
```
"C:\Program Files\Google\Chrome\Application\chrome.exe" http://twitch.tv/shadowdao
```
```"C:\Program Files\Google\Chrome\Application\chrome.exe" http://twitch.tv/shadowdao``` #### Run Notepad
```
notepad.exe
```
#### Open File Explorer to a specific location
```
explorer.exe "C:\Users\YourUsername\Documents"
```
### Linux Examples
#### Opening Firefox
```
firefox https://example.com
```
#### Opening Steam
```
steam steam://rungameid/2767030
```
#### Launch Terminal
```
gnome-terminal
```
#### Open File Manager
```
nautilus ~/Documents
```
### macOS Examples
#### Opening Safari
```
open -a Safari https://example.com
```
#### Opening Terminal
```
open -a Terminal
```
#### Open Finder to a specific location
```
open ~/Documents
```
#### Launch Applications
```
open -a "Visual Studio Code"
```
#### Special Thanks to CatArgent_ on Twitch for proof reading my stuff and providing valuable feedback. #### Special Thanks to CatArgent_ on Twitch for proof reading my stuff and providing valuable feedback.
+1 -1
View File
@@ -1,6 +1,6 @@
# Configuration and constants for MacroPad Server # Configuration and constants for MacroPad Server
VERSION = "0.5.4 Beta" VERSION = "0.8.5 Beta"
DEFAULT_PORT = 40000 DEFAULT_PORT = 40000
# UI Theme colors # UI Theme colors
Binary file not shown.
+2 -2
View File
@@ -2,7 +2,7 @@
a = Analysis( a = Analysis(
['mp-server-v2.py'], ['main.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[], datas=[],
@@ -22,7 +22,7 @@ exe = EXE(
a.binaries, a.binaries,
a.datas, a.datas,
[], [],
name='mp-server-v2', name='macropad',
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.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='macropad',
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,
)
+45
View File
@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.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='macropad',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=True,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
app = BUNDLE(
exe,
name='macropad.app',
icon=None,
bundle_identifier=None,
)
+76 -16
View File
@@ -9,7 +9,7 @@ import socket
import qrcode import qrcode
import webbrowser import webbrowser
import pystray import pystray
from PIL import Image, ImageTk from PIL import Image, ImageTk, ImageDraw, ImageFont
from config import VERSION, DEFAULT_PORT, THEME from config import VERSION, DEFAULT_PORT, THEME
from macro_manager import MacroManager from macro_manager import MacroManager
@@ -46,12 +46,20 @@ class MacroPadServer:
# Server state # Server state
self.server_running = False self.server_running = False
self.flask_thread = None self.flask_thread = None
# Tray state
self.tray_icon = None
self.is_closing = False
# Create UI # Create UI
self.create_ui() self.create_ui()
# Set up window close handler # Set up window event handlers
self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.root.bind('<Unmap>', self.on_minimize)
# Initialize tray icon
self.create_tray_icon()
def configure_styles(self): def configure_styles(self):
"""Configure the dark theme styles""" """Configure the dark theme styles"""
@@ -68,8 +76,10 @@ class MacroPadServer:
style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0) style.configure("TNotebook", background=THEME['bg_color'], borderwidth=0)
style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'], style.configure("TNotebook.Tab", background=THEME['tab_bg'], foreground=THEME['fg_color'],
padding=[12, 8], borderwidth=0) padding=[12, 8], borderwidth=0)
style.map("TNotebook.Tab", background=[("selected", THEME['tab_selected'])], style.map("TNotebook.Tab",
foreground=[("selected", THEME['fg_color'])]) background=[("selected", THEME['tab_selected'])],
foreground=[("selected", THEME['fg_color'])],
padding=[("selected", [12, 8])]) # Keep same padding when selected
def create_ui(self): def create_ui(self):
"""Create the main user interface""" """Create the main user interface"""
@@ -406,15 +416,53 @@ class MacroPadServer:
if self.server_running: if self.server_running:
webbrowser.open(f"http://localhost:{DEFAULT_PORT}") webbrowser.open(f"http://localhost:{DEFAULT_PORT}")
def on_minimize(self, event):
"""Handle window minimize event"""
# Only minimize to tray if the window is being iconified, not just unmapped
if event.widget == self.root and self.root.state() == 'iconic':
self.root.withdraw() # Hide window
def create_tray_icon(self): def create_tray_icon(self):
"""Create system tray icon""" """Create system tray icon"""
icon_image = Image.new("RGB", (64, 64), THEME['accent_color']) try:
menu = ( # Create a simple icon image with M letter
pystray.MenuItem('Show', self.show_window), icon_image = Image.new("RGB", (64, 64), THEME['accent_color'])
pystray.MenuItem('Exit', self.exit_app) draw = ImageDraw.Draw(icon_image)
)
self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu) try:
self.tray_icon.run_detached() # Try to use a system font
font = ImageFont.truetype("arial.ttf", 40)
except:
try:
# Try other common fonts
font = ImageFont.truetype("calibri.ttf", 40)
except:
# Fall back to default font
font = ImageFont.load_default()
# Draw "MP" in the center
text = "MP"
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (64 - text_width) // 2
y = (64 - text_height) // 2
draw.text((x, y), text, fill="white", font=font)
menu = (
pystray.MenuItem('Show', self.show_window),
pystray.MenuItem('Exit', self.exit_app)
)
self.tray_icon = pystray.Icon("macropad", icon_image, "MacroPad Server", menu)
# Run tray icon in a separate thread
tray_thread = threading.Thread(target=self.tray_icon.run, daemon=True)
tray_thread.start()
except Exception as e:
print(f"Error creating tray icon: {e}")
# Tray icon is optional, continue without it
def show_window(self, icon=None, item=None): def show_window(self, icon=None, item=None):
"""Show window from tray""" """Show window from tray"""
@@ -425,14 +473,26 @@ class MacroPadServer:
def exit_app(self, icon=None, item=None): def exit_app(self, icon=None, item=None):
"""Exit the application""" """Exit the application"""
self.is_closing = True
self.stop_server() self.stop_server()
if hasattr(self, 'tray_icon'):
self.tray_icon.stop() if self.tray_icon:
self.root.quit() try:
self.tray_icon.stop()
except:
pass
try:
self.root.quit()
except:
pass
# Force exit if needed
import os
os._exit(0)
def on_closing(self): def on_closing(self):
"""Handle window close event""" """Handle window close event - exit the application"""
# For now just exit, but could minimize to tray
self.exit_app() self.exit_app()
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.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='main',
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,
)
File diff suppressed because it is too large Load Diff
-1176
View File
File diff suppressed because it is too large Load Diff
-971
View File
@@ -1,971 +0,0 @@
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 socket
import qrcode
import sys
import time
class MacroPadServer:
def __init__(self, root):
self.root = root
self.root.title("MacroPad Server")
self.root.geometry("800x600")
self.configure_styles()
# Set Version Str
self.version_str = "0.5.4 Beta"
# 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__))
self.data_file = os.path.join(base_dir, "macros.json")
self.images_dir = os.path.join(base_dir, "macro_images")
self.app_dir = base_dir
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))
#Set version details
version_label = tk.Label(self.root, text=self.version_str,
bg=self.bg_color, fg=self.fg_color,
font=('Helvetica', 8)) # Using smaller font for version text
version_label.pack(side=tk.BOTTOM, anchor=tk.SE, padx=5, pady=(0, 2))
# 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(
"<Configure>",
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(self.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 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 the systems internal IP address
ip_address = self.get_ip_addresses()
if ip_address:
# Set the URL display
url = f"http://{ip_address}:{self.server_port}"
url_text = "Web UI available at:\n" + "\n" + url
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(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):
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):
"""Get the primary internal IPv4 address of the machine."""
try:
# Create a socket to connect to an external server
# This helps determine which network interface is used for outbound connections
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# We don't need to actually send data - just configure the socket
s.connect(("8.8.8.8", 80))
# Get the IP address that would be used for this connection
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
print(f"Error getting IP address: {e}")
# Fallback method if the above doesn't work
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
# Don't return localhost address
if ip.startswith("127."):
for addr_info in socket.getaddrinfo(hostname, None):
potential_ip = addr_info[4][0]
# If IPv4 and not localhost
if '.' in potential_ip and not potential_ip.startswith("127."):
return potential_ip
else:
return ip
except:
return "127.0.0.1" # Last resort fallback
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 = '''
<!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;
}
.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;
}
}
</style>
</head>
<body>
<div class="header-container">
<h1>MacroPad Web Interface</h1>
<button class="refresh-button" onclick="loadMacros()">
<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="macro-grid" id="macro-grid">
<!-- Macros will be loaded here -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadMacros();
});
function loadMacros() {
fetch('/api/macros')
.then(response => response.json())
.then(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 defined.</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);
}
})
.catch(error => {
console.error('Error loading macros:', error);
});
}
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>
'''
@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/<path:image_path>')
def get_image(image_path):
try:
# Using os.path.join would translate slashes incorrectly on Windows
# Use normpath instead
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():
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", {})
# 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')
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
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.stop_server()
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()
+7 -7
View File
@@ -217,7 +217,7 @@ class TabManager:
"""Show tab management dialog""" """Show tab management dialog"""
dialog = tk.Toplevel(self.parent) dialog = tk.Toplevel(self.parent)
dialog.title("Manage Tabs") dialog.title("Manage Tabs")
dialog.geometry("400x300") dialog.geometry("450x400") # Increased width and height
dialog.transient(self.parent) dialog.transient(self.parent)
dialog.configure(bg=THEME['bg_color']) dialog.configure(bg=THEME['bg_color'])
dialog.grab_set() dialog.grab_set()
@@ -227,12 +227,12 @@ class TabManager:
bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=10) bg=THEME['bg_color'], fg=THEME['fg_color']).pack(pady=10)
# Create scrollable frame # Create scrollable frame
list_frame = ttk.Frame(dialog) list_frame = tk.Frame(dialog, bg=THEME['bg_color'])
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
canvas = tk.Canvas(list_frame, bg=THEME['bg_color'], highlightthickness=0) canvas = tk.Canvas(list_frame, bg=THEME['bg_color'], highlightthickness=0)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview) scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas) scrollable_frame = tk.Frame(canvas, bg=THEME['bg_color'])
scrollable_frame.bind( scrollable_frame.bind(
"<Configure>", "<Configure>",
@@ -247,7 +247,7 @@ class TabManager:
# Category entries for each macro # Category entries for each macro
category_vars = {} category_vars = {}
for macro_id, macro in self.macro_manager.macros.items(): for macro_id, macro in self.macro_manager.macros.items():
frame = ttk.Frame(scrollable_frame) frame = tk.Frame(scrollable_frame, bg=THEME['bg_color'])
frame.pack(fill="x", pady=2, padx=5) frame.pack(fill="x", pady=2, padx=5)
tk.Label(frame, text=macro["name"], bg=THEME['bg_color'], fg=THEME['fg_color'], tk.Label(frame, text=macro["name"], bg=THEME['bg_color'], fg=THEME['fg_color'],
@@ -259,9 +259,9 @@ class TabManager:
bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color']) bg=THEME['highlight_color'], fg=THEME['fg_color'], insertbackground=THEME['fg_color'])
entry.pack(side=tk.RIGHT, padx=(5, 0)) entry.pack(side=tk.RIGHT, padx=(5, 0))
# Buttons # Buttons - use a fixed frame at bottom
button_frame = ttk.Frame(dialog) button_frame = tk.Frame(dialog, bg=THEME['bg_color'])
button_frame.pack(fill=tk.X, pady=10) button_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10)
def save_categories(): def save_categories():
for macro_id, category_var in category_vars.items(): for macro_id, category_var in category_vars.items():
+1 -1
View File
@@ -1 +1 @@
0.5.4 0.8.5