MP-Server/mp-server.py

646 lines
28 KiB
Python

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()