security+robustness(py/gui): thread-safety, upload hardening, keyring
macro_manager: - All macro reads/writes guarded by a lock; macros.json written atomically (temp + fsync + os.replace). Commands are copied under the lock and executed outside it so wait/keyboard steps don't block other threads. - last_used disk writes debounced (>=5s) instead of a full rewrite per press. - Validate key/hotkey names against pyautogui.KEYBOARD_KEYS and clamp wait ms; invalid input is skipped/logged, not blindly executed. web_server: - WebSocket receive loop always disconnects (fixes dead-socket leak); broadcast prunes failed sockets. - POST /api/execute offloaded to a thread executor so a long macro no longer blocks the event loop. - Image upload hardened: 5MB cap (413), real-image validation via Pillow, server-generated name under macro_images/, returns a relative path only, cleans up on failure. gui: - Execute macros off the Qt main thread (signal back to status bar); debounce resizeEvent and only rebuild when the column count changes. - Relay password stored in the OS keyring when available (plaintext JSON blanked; graceful fallback if no backend); delete dialogs name the macro; honor the minimize-to-tray setting; surface save failures. - Add media/system keys (volume, play/pause, track nav) to the macro editor. - THEME gains shared danger/accent-hover colors; drop dead imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
-16
@@ -1,19 +1,30 @@
|
||||
# Macro editor dialog with command builder (PySide6)
|
||||
|
||||
import os
|
||||
from typing import Optional, List, Dict
|
||||
from typing import Optional, List
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
|
||||
QComboBox, QSpinBox, QMessageBox, QFileDialog, QWidget,
|
||||
QGroupBox, QScrollArea, QCompleter
|
||||
QComboBox, QMessageBox, QFileDialog, QWidget,
|
||||
QGroupBox, QScrollArea
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
from config import THEME, IMAGE_EXTENSIONS
|
||||
|
||||
# Selectable single keys offered in the key picker. Includes common media/system
|
||||
# keys pyautogui supports, a near-zero-cost win for a media/stream deck.
|
||||
KEY_CHOICES = [
|
||||
"Enter", "Tab", "Escape", "Space", "Backspace", "Delete",
|
||||
"Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown",
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
||||
# Media / system keys
|
||||
"VolumeUp", "VolumeDown", "VolumeMute",
|
||||
"PlayPause", "NextTrack", "PrevTrack", "Stop",
|
||||
]
|
||||
|
||||
|
||||
class CommandItem(QWidget):
|
||||
"""Widget representing a single command in the list."""
|
||||
@@ -86,7 +97,7 @@ class CommandItem(QWidget):
|
||||
del_btn = QPushButton("X")
|
||||
del_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: #dc3545;
|
||||
background-color: {THEME['danger_color']};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 8px;
|
||||
@@ -94,7 +105,7 @@ class CommandItem(QWidget):
|
||||
min-height: 20px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #c82333;
|
||||
background-color: {THEME['danger_hover']};
|
||||
}}
|
||||
""")
|
||||
del_btn.setFixedSize(30, 28)
|
||||
@@ -212,9 +223,7 @@ class CommandBuilder(QWidget):
|
||||
|
||||
elif cmd_type == "key":
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
keys = ["Enter", "Tab", "Escape", "Space", "Backspace", "Delete",
|
||||
"Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown",
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
|
||||
keys = list(KEY_CHOICES)
|
||||
key, ok = QInputDialog.getItem(self, "Key Command", "Select key:", keys, 0, True)
|
||||
if not ok or not key:
|
||||
return
|
||||
@@ -295,9 +304,7 @@ class CommandBuilder(QWidget):
|
||||
cmd["value"] = text
|
||||
|
||||
elif cmd_type == "key":
|
||||
keys = ["Enter", "Tab", "Escape", "Space", "Backspace", "Delete",
|
||||
"Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown",
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
|
||||
keys = list(KEY_CHOICES)
|
||||
keys_lower = [k.lower() for k in keys]
|
||||
current = keys_lower.index(cmd.get("value", "enter")) if cmd.get("value") in keys_lower else 0
|
||||
key, ok = QInputDialog.getItem(self, "Edit Key", "Select key:", keys, current, True)
|
||||
@@ -511,7 +518,7 @@ class MacroEditorDialog(QDialog):
|
||||
delete_btn = QPushButton("Delete")
|
||||
delete_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: #dc3545;
|
||||
background-color: {THEME['danger_color']};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
@@ -519,7 +526,7 @@ class MacroEditorDialog(QDialog):
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #c82333;
|
||||
background-color: {THEME['danger_hover']};
|
||||
}}
|
||||
""")
|
||||
delete_btn.clicked.connect(self.delete_macro)
|
||||
@@ -554,7 +561,7 @@ class MacroEditorDialog(QDialog):
|
||||
font-weight: bold;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #0096ff;
|
||||
background-color: {THEME['accent_hover']};
|
||||
}}
|
||||
""")
|
||||
save_btn.clicked.connect(self.save_macro)
|
||||
@@ -634,9 +641,15 @@ class MacroEditorDialog(QDialog):
|
||||
|
||||
def delete_macro(self):
|
||||
"""Delete the current macro."""
|
||||
macro_name = self.name_input.text().strip()
|
||||
prompt = (
|
||||
f"Are you sure you want to delete \"{macro_name}\"?"
|
||||
if macro_name else
|
||||
"Are you sure you want to delete this macro?"
|
||||
)
|
||||
reply = QMessageBox.question(
|
||||
self, "Delete Macro",
|
||||
"Are you sure you want to delete this macro?",
|
||||
prompt,
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
|
||||
Reference in New Issue
Block a user