from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from lib.config_manager import ConfigurationManager from lib.launcher_manager import LauncherManager import os import json # Added import from datetime import datetime import time # Added for shutdown delay import sys # Added for platform detection import subprocess # Add this to the imports at the top import shutil # For shutil.whichimport os # --- Imports for System Tray Icon --- import threading import webbrowser import sys import requests # To send shutdown request from PIL import Image import pystray app = Flask( __name__, static_url_path="", static_folder="static", template_folder="templates" ) sock = Sock(app) config_manager = ConfigurationManager() # Inicializar launcher manager launcher_manager = LauncherManager(config_manager.data_path) # Lista global para mantener las conexiones WebSocket activas websocket_connections = set() # --- Globals for Tray Icon --- tray_icon = None # --- Parámetros para envío por lotes de logs --- BATCH_FLUSH_INTERVAL = 0.5 # segundos broadcast_buffer = [] # Almacena líneas formateadas pendientes de envío buffer_lock = threading.Lock() # Sincroniza acceso al buffer def _broadcast_flush_loop(): """Hilo que vacía el buffer de logs cada BATCH_FLUSH_INTERVAL segundos.""" while True: time.sleep(BATCH_FLUSH_INTERVAL) with buffer_lock: if not broadcast_buffer: continue batch = "\n".join(broadcast_buffer) broadcast_buffer.clear() _send_batch_to_clients(batch) def _send_batch_to_clients(batch_message: str): """Envía un bloque de texto a todas las conexiones WebSocket activas.""" dead_connections = set() for ws in list(websocket_connections): try: if ws.connected: ws.send(batch_message + "\n") except Exception: dead_connections.add(ws) websocket_connections.difference_update(dead_connections) # Iniciar hilo de vaciado en segundo plano (ahora que las dependencias están definidas) flusher_thread = threading.Thread(target=_broadcast_flush_loop, daemon=True) flusher_thread.start() @sock.route("/ws") def handle_websocket(ws): try: websocket_connections.add(ws) while True: message = ws.receive() if message: broadcast_message(message) except Exception as e: print(f"WebSocket error: {e}") finally: websocket_connections.remove(ws) def broadcast_message(message): """Acumula mensajes en un buffer y los envía por lotes cada 500 ms.""" timestamp = datetime.now().strftime("[%H:%M:%S] ") # Normalizar entrada a lista de mensajes if isinstance(message, list): messages = message else: messages = [line.strip() for line in message.splitlines() if line.strip()] for raw_msg in messages: # Limpiar timestamps duplicados al inicio del mensaje while raw_msg.startswith("[") and "]" in raw_msg: try: closing_bracket = raw_msg.index("]") + 1 if raw_msg[1:closing_bracket - 1].replace(":", "").isdigit(): raw_msg = raw_msg[closing_bracket:].strip() else: break except ValueError: break # Registrar en archivo (la clase Logger añade timestamp propio) config_manager.append_log(raw_msg) # Formatear para el WebSocket y añadir al buffer formatted_msg_for_ws = f"{timestamp}{raw_msg}" with buffer_lock: broadcast_buffer.append(formatted_msg_for_ws) @app.route("/api/execute_script", methods=["POST"]) def execute_script(): try: script_group = request.json["group"] script_name = request.json["script"] # Ejecutar el script y obtener resultado result = config_manager.execute_script( script_group, script_name, broadcast_message ) return jsonify(result) except Exception as e: error_msg = f"Error ejecutando script: {str(e)}" broadcast_message(error_msg) return jsonify({"error": error_msg}) @app.route("/") def index(): script_groups = config_manager.get_script_groups() # Para ordenar una lista de diccionarios, necesitamos especificar una clave. # Asumimos que cada diccionario tiene una clave 'name' por la cual ordenar. sorted_script_groups = sorted(script_groups, key=lambda group: group['name']) return render_template("index.html", script_groups=sorted_script_groups) @app.route("/api/config/", methods=["GET", "POST"]) def handle_config(level): group = request.args.get("group") if request.method == "GET": try: return jsonify(config_manager.get_config(level, group)) except FileNotFoundError: return jsonify({}) else: data = request.json config_manager.update_config(level, data, group) return jsonify({"status": "success"}) @app.route("/api/schema/", methods=["GET", "POST"]) def handle_schema(level): group = request.args.get("group") if request.method == "GET": return jsonify(config_manager.get_schema(level, group)) else: data = request.json config_manager.update_schema(level, data, group) return jsonify({"status": "success"}) @app.route("/api/scripts/") def get_scripts(group): # list_scripts ahora devuelve detalles y filtra los ocultos scripts = config_manager.list_scripts(group) # El frontend espera 'name' y 'description', mapeamos desde 'display_name' y 'short_description' return jsonify( [ { "name": s["display_name"], "description": s["short_description"], "filename": s["filename"], "long_description": s["long_description"], } for s in scripts ] ) @app.route("/api/working-directory", methods=["POST"]) def set_working_directory(): data = request.json if not data: return jsonify({"status": "error", "message": "No data provided"}) path = data.get("path") group = data.get("group") if not path or not group: return jsonify( { "status": "error", "message": f"Missing required fields. Path: {path}, Group: {group}", } ) print(f"Setting working directory - Path: {path}, Group: {group}") # Debug line return jsonify(config_manager.set_work_dir(group, path)) @app.route("/api/working-directory/", methods=["GET"]) def get_working_directory(group): path = config_manager.get_work_dir(group) return jsonify({"path": path, "status": "success" if path else "not_set"}) @app.route("/api/browse-directories") def browse_directories(): import tkinter as tk from tkinter import filedialog # Obtener el directorio inicial current_dir = request.args.get("current_path") if not current_dir or not os.path.exists(current_dir): current_dir = os.path.dirname(os.path.abspath(__file__)) # Crear y configurar la ventana principal de tkinter root = tk.Tk() root.attributes("-topmost", True) # Mantener la ventana siempre arriba root.withdraw() # Abrir el diálogo de selección de directorio directory = filedialog.askdirectory( initialdir=current_dir, title="Seleccionar Directorio de Trabajo" ) # Destruir la ventana de tkinter root.destroy() if directory: return jsonify({"status": "success", "path": directory}) return jsonify({"status": "cancelled"}) @app.route("/api/logs", methods=["GET", "DELETE"]) def handle_logs(): if request.method == "GET": return jsonify({"logs": config_manager.read_log()}) else: # DELETE success = config_manager.clear_log() return jsonify({"status": "success" if success else "error"}) @app.route("/api/group-description/", methods=["GET", "POST"]) def handle_group_description(group): if request.method == "GET": try: details = config_manager.get_group_details(group) if "error" in details: return jsonify(details), 404 # Group not found return jsonify(details) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 else: # POST try: data = request.json result = config_manager.update_group_description(group, data) return jsonify(result) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 @app.route("/api/script-details//", methods=["GET", "POST"]) def handle_script_details(group, script_filename): if request.method == "GET": try: details = config_manager.get_script_details(group, script_filename) return jsonify(details) except Exception as e: print(f"Error getting script details for {group}/{script_filename}: {e}") return jsonify({"status": "error", "message": str(e)}), 500 else: # POST try: data = request.json result = config_manager.update_script_details(group, script_filename, data) return jsonify(result) except Exception as e: print(f"Error updating script details for {group}/{script_filename}: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route("/api/directory-history/") def get_directory_history(group): history = config_manager.get_directory_history(group) return jsonify(history) @app.route("/api/open-vscode/", methods=["POST"]) def open_group_in_vscode(group): try: # Get the full path to the script group directory script_group_path = os.path.join(config_manager.script_groups_path, group) if not os.path.isdir(script_group_path): return ( jsonify( { "status": "error", "message": f"Directorio del grupo '{group}' no encontrado", } ), 404, ) # VS Code executable path vscode_path = ( r"C:\Users\migue\AppData\Local\Programs\Microsoft VS Code\Code.exe" ) # Check if the file exists if not os.path.isfile(vscode_path): return ( jsonify( { "status": "error", "message": f"VS Code no encontrado en: {vscode_path}", } ), 404, ) print(f"Launching VS Code from: {vscode_path}") print(f"Opening directory: {script_group_path}") # Try with shell=True which can help with Windows path issues process = subprocess.Popen(f'"{vscode_path}" "{script_group_path}"', shell=True) # Log the process ID for debugging print(f"Process started with PID: {process.pid}") return jsonify( {"status": "success", "message": f"VS Code abierto en: {script_group_path}"} ) except Exception as e: print(f"Error opening VS Code for group '{group}': {str(e)}") return ( jsonify( {"status": "error", "message": f"Error al abrir VS Code: {str(e)}"} ), 500, ) @app.route("/api/open-miniconda", methods=["POST"]) def open_miniconda_console(): try: # Path to the Miniconda installation miniconda_path = r"C:\Users\migue\miniconda3" # Check if directory exists if not os.path.isdir(miniconda_path): return ( jsonify( { "status": "error", "message": f"Miniconda no encontrado en: {miniconda_path}", } ), 404, ) # Path to the activate script activate_path = os.path.join(miniconda_path, "Scripts", "activate.bat") if not os.path.isfile(activate_path): return ( jsonify( { "status": "error", "message": f"Script de activación no encontrado en: {activate_path}", } ), 404, ) print(f"Opening Miniconda Console from: {miniconda_path}") # Use subprocess with CREATE_NEW_CONSOLE flag to ensure the window appears # Start the Windows command processor and tell it to run the activate batch file startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = 1 # SW_SHOWNORMAL # Run cmd.exe with specific flags to show a window process = subprocess.Popen( [ "cmd.exe", "/K", f"echo Activating Miniconda environment... && " f'"{activate_path}" && ' f"echo Miniconda activated successfully. && " f"cd /d {miniconda_path}", ], creationflags=subprocess.CREATE_NEW_CONSOLE, startupinfo=startupinfo, ) print(f"Miniconda Console process started with PID: {process.pid}") return jsonify( {"status": "success", "message": "Miniconda Console abierta correctamente"} ) except Exception as e: print(f"Error opening Miniconda Console: {str(e)}") return ( jsonify( { "status": "error", "message": f"Error al abrir Miniconda Console: {str(e)}", } ), 500, ) @app.route("/api/open-explorer", methods=["POST"]) def open_explorer_route(): data = request.json frontend_path = data.get("path") group = data.get("group") if not group: return jsonify({"status": "error", "message": "Grupo no proporcionado."}), 400 if not frontend_path: return jsonify({"status": "error", "message": "Ruta no proporcionada."}), 400 # Obtener el directorio de trabajo configurado y normalizado para el grupo configured_work_dir_raw = config_manager.get_work_dir(group) if not configured_work_dir_raw: return jsonify({"status": "error", "message": f"No hay directorio de trabajo configurado para el grupo '{group}'."}), 404 configured_work_dir_abs = os.path.abspath(configured_work_dir_raw) frontend_path_abs = os.path.abspath(frontend_path) # Validar que la ruta del frontend coincide con la ruta configurada if configured_work_dir_abs != frontend_path_abs: print(f"Intento de acceso no válido: Grupo '{group}', Frontend Path '{frontend_path_abs}', Configured Path '{configured_work_dir_abs}'") return jsonify({"status": "error", "message": "La ruta proporcionada no coincide con el directorio de trabajo seguro para este grupo."}), 403 # Validar que la ruta (ahora sabemos que es la configurada) realmente existe if not os.path.isdir(configured_work_dir_abs): return jsonify({"status": "error", "message": f"El directorio de trabajo configurado '{configured_work_dir_abs}' no es un directorio válido o no existe."}), 400 try: if sys.platform == "win32": os.startfile(configured_work_dir_abs) elif sys.platform == "darwin": # macOS subprocess.Popen(["open", configured_work_dir_abs]) else: # linux variants subprocess.Popen(["xdg-open", configured_work_dir_abs]) return jsonify({"status": "success", "message": f"Abriendo '{configured_work_dir_abs}' en el explorador."}) except Exception as e: error_msg = f"Error al abrir el explorador en '{configured_work_dir_abs}': {str(e)}" print(error_msg) return jsonify({"status": "error", "message": error_msg}), 500 # --- System Tray Icon Functions --- def run_flask(): """Runs the Flask app.""" print("Starting Flask server on http://127.0.0.1:5000/") try: # use_reloader=False is important when running in a thread # For production, consider using waitress or gunicorn instead of app.run app.run(host="127.0.0.1", port=5000, debug=True, use_reloader=False) except Exception as e: print(f"Error running Flask app: {e}") # Optionally try to stop the tray icon if Flask fails critically if tray_icon: print("Attempting to stop tray icon due to Flask error.") tray_icon.stop() def open_app_browser(icon, item): """Callback function to open the browser.""" print("Opening application in browser...") webbrowser.open("http://127.0.0.1:5000/") def shutdown_flask_server(): """Attempts to gracefully shut down the Werkzeug server.""" try: # This requires the development server (werkzeug) # Send a request to a special shutdown route requests.post("http://127.0.0.1:5000/_shutdown", timeout=1) except Exception as e: print(f"Could not send shutdown request to Flask server: {e}") print("Flask server might need to be closed manually.") def stop_icon_thread(): """Helper function to stop the icon after a delay, allowing HTTP response.""" time.sleep(0.1) # Small delay to allow the HTTP response to be sent if tray_icon: print("Stopping tray icon from shutdown route...") tray_icon.stop() else: print("Tray icon not available to stop.") # As a last resort if the icon isn't running for some reason # print("Attempting os._exit(0) as fallback.") # os._exit(0) # Force exit - use with caution @app.route("/_shutdown", methods=["POST"]) def shutdown_route(): """Internal route to shut down the application via the tray icon.""" print("Shutdown endpoint called.") # Stop the main application thread by stopping the tray icon. # Do this in a separate thread to allow the HTTP response to return first. stopper = threading.Thread(target=stop_icon_thread, daemon=True) stopper.start() print("Shutdown signal sent to tray icon thread.") return jsonify(status="success", message="Application shutdown initiated..."), 200 def exit_application(icon, item): """Callback function to exit the application.""" print("Exit requested via tray menu.") # Just stop the icon. This will end the main thread, and the daemon Flask thread will exit. print("Stopping tray icon...") if icon: # pystray passes the icon object icon.stop() elif tray_icon: # Fallback just in case tray_icon.stop() # === LAUNCHER GUI APIs === @app.route("/api/launcher-groups", methods=["GET", "POST"]) def handle_launcher_groups(): """Gestionar grupos de launcher (GET: obtener, POST: crear)""" if request.method == "GET": try: groups = launcher_manager.get_launcher_groups() return jsonify(groups) except Exception as e: return jsonify({"error": str(e)}), 500 else: # POST try: data = request.json result = launcher_manager.add_launcher_group(data) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-groups/", methods=["GET", "PUT", "DELETE"]) def handle_launcher_group(group_id): """Gestionar grupo específico (GET: obtener, PUT: actualizar, DELETE: eliminar)""" if request.method == "GET": try: group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({"error": "Group not found"}), 404 return jsonify(group) except Exception as e: return jsonify({"error": str(e)}), 500 elif request.method == "PUT": try: data = request.json result = launcher_manager.update_launcher_group(group_id, data) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 else: # DELETE try: result = launcher_manager.delete_launcher_group(group_id) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-scripts/") def get_launcher_scripts(group_id): """Obtener scripts de un grupo del launcher""" try: scripts = launcher_manager.get_group_scripts(group_id) return jsonify(scripts) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-scripts-all/") def get_all_launcher_scripts(group_id): """Obtener TODOS los scripts de un grupo (incluyendo ocultos) para gestión""" try: scripts = launcher_manager.get_all_group_scripts(group_id) return jsonify(scripts) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-script-metadata//", methods=["GET", "POST"]) def handle_launcher_script_metadata(group_id, script_name): """Gestionar metadatos de un script específico""" if request.method == "GET": try: metadata = launcher_manager.get_script_metadata(group_id, script_name) return jsonify(metadata) except Exception as e: return jsonify({"error": str(e)}), 500 else: # POST try: data = request.json result = launcher_manager.update_script_metadata(group_id, script_name, data) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/python-environments") def get_python_environments(): """Obtener entornos de Python disponibles""" try: envs = launcher_manager.get_available_python_envs() return jsonify(envs) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/execute-gui-script", methods=["POST"]) def execute_gui_script(): """Ejecutar script GUI con argumentos opcionales""" try: data = request.json group_id = data["group_id"] script_name = data["script_name"] script_args = data.get("args", []) working_dir = data.get("working_dir", None) use_pythonw = data.get("use_pythonw", False) # Por defecto python.exe para logging result = launcher_manager.execute_gui_script( group_id, script_name, script_args, broadcast_message, working_dir, use_pythonw ) return jsonify(result) except Exception as e: error_msg = f"Error ejecutando script GUI: {str(e)}" broadcast_message(error_msg) return jsonify({"error": error_msg}), 500 @app.route("/api/launcher-favorites", methods=["GET", "POST"]) def handle_launcher_favorites(): """Gestionar favoritos del launcher""" if request.method == "GET": try: favorites = launcher_manager.get_favorites() return jsonify({"favorites": favorites}) except Exception as e: return jsonify({"error": str(e)}), 500 else: # POST try: data = request.json group_id = data["group_id"] script_name = data["script_name"] result = launcher_manager.toggle_favorite(group_id, script_name) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-history", methods=["GET", "DELETE"]) def handle_launcher_history(): """Gestionar historial del launcher""" if request.method == "GET": try: history = launcher_manager.get_history() return jsonify({"history": history}) except Exception as e: return jsonify({"error": str(e)}), 500 else: # DELETE try: result = launcher_manager.clear_history() return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-categories") def get_launcher_categories(): """Obtener categorías disponibles del launcher""" try: categories = launcher_manager.get_categories() return jsonify(categories) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/group-icon//") def get_group_icon(launcher_type, group_id): """Obtener icono de un grupo (config o launcher)""" try: if launcher_type == "launcher": group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({"error": "Group not found"}), 404 icon_path = os.path.join(group["directory"], "icon.ico") if os.path.exists(icon_path): from flask import send_file return send_file(icon_path, mimetype='image/x-icon') elif launcher_type == "config": group_path = os.path.join(config_manager.script_groups_path, group_id) icon_path = os.path.join(group_path, "icon.ico") if os.path.exists(icon_path): from flask import send_file return send_file(icon_path, mimetype='image/x-icon') # Icono por defecto - devolver datos para que el frontend genere el icono return jsonify({"type": "default", "icon": "📁"}) except Exception as e: return jsonify({"error": str(e)}), 500 # Nuevas APIs para gestión de procesos y Markdown @app.route("/api/launcher-process-focus/", methods=["POST"]) def focus_launcher_process(pid): """Activar foco de un proceso""" try: result = launcher_manager.focus_process(pid) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-process-terminate/", methods=["POST"]) def terminate_launcher_process(pid): """Cerrar un proceso""" try: result = launcher_manager.terminate_process(pid) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-running-processes") def get_launcher_running_processes(): """Obtener procesos en ejecución""" try: processes = launcher_manager.get_running_processes() return jsonify({"processes": processes}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/launcher-open-vscode/", methods=["POST"]) def open_launcher_group_in_vscode(group_id): """Abrir grupo del launcher en VS Code""" try: group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({"status": "error", "message": "Grupo no encontrado"}), 404 script_group_path = group["directory"] if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo '{group['name']}' no encontrado" }), 404 # VS Code executable path vscode_path = r"C:\Users\migue\AppData\Local\Programs\Microsoft VS Code\Code.exe" if not os.path.isfile(vscode_path): return jsonify({ "status": "error", "message": f"VS Code no encontrado en: {vscode_path}" }), 404 print(f"Launching VS Code for launcher group: {group['name']}") print(f"Opening directory: {script_group_path}") process = subprocess.Popen(f'"{vscode_path}" "{script_group_path}"', shell=True) print(f"Process started with PID: {process.pid}") return jsonify({ "status": "success", "message": f"VS Code abierto en: {script_group_path}" }) except Exception as e: print(f"Error opening VS Code for launcher group '{group_id}': {str(e)}") return jsonify({ "status": "error", "message": f"Error al abrir VS Code: {str(e)}" }), 500 @app.route("/api/launcher-markdown/") def get_launcher_markdown_files(group_id): """Obtener archivos Markdown de un grupo""" try: markdown_files = launcher_manager.get_markdown_files(group_id) return jsonify({"files": markdown_files}) except Exception as e: print(f"Error getting markdown files for group {group_id}: {e}") # Devolver lista vacía en lugar de error para no interferir con scripts return jsonify({"files": []}) @app.route("/api/launcher-markdown-content//") def get_launcher_markdown_content(group_id, relative_path): """Obtener contenido de un archivo Markdown""" try: result = launcher_manager.read_markdown_file(group_id, relative_path) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 # --- Global Error Handler (for debugging unhandled exceptions) --- @app.errorhandler(Exception) def handle_unhandled_exception(e): # Log the error with traceback app.logger.error('Unhandled Exception: %s', e, exc_info=True) # Return a JSON response for API calls, or HTML for others if request.path.startswith('/api/'): return jsonify({"status": "error", "message": f"Internal Server Error: {str(e)}"}), 500 else: return "

Internal Server Error

An unhandled error occurred.

", 500 # === FIN LAUNCHER GUI APIs === # --- Helper function to find VS Code --- def find_vscode_executable(): """Intenta encontrar el ejecutable de VS Code en ubicaciones comunes y en el PATH.""" # Comprobar la variable de entorno VSCODE_PATH primero (si la defines) vscode_env_path = os.getenv("VSCODE_PATH") if vscode_env_path and os.path.isfile(vscode_env_path): return vscode_env_path common_paths = [] local_app_data = os.getenv('LOCALAPPDATA') if local_app_data: common_paths.append(os.path.join(local_app_data, r"Programs\Microsoft VS Code\Code.exe")) common_paths.extend([ r"C:\Program Files\Microsoft VS Code\Code.exe", r"C:\Program Files (x86)\Microsoft VS Code\Code.exe", ]) for path in common_paths: if os.path.isfile(path): return path return shutil.which("code") # Busca 'code' en el PATH @app.route("/api/open-editor///", methods=["POST"]) def open_group_in_editor(editor, group_system, group_id): """Ruta unificada para abrir grupos en diferentes editores""" try: # Validar editor if editor not in ['vscode', 'cursor']: return jsonify({ "status": "error", "message": f"Editor '{editor}' no soportado. Usar 'vscode' o 'cursor'" }), 400 # Determinar directorio según el sistema if group_system == 'config': script_group_path = os.path.join(config_manager.script_groups_path, group_id) if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo config '{group_id}' no encontrado" }), 404 elif group_system == 'launcher': group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({ "status": "error", "message": f"Grupo launcher '{group_id}' no encontrado" }), 404 script_group_path = group["directory"] if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo launcher '{group['name']}' no encontrado" }), 404 else: return jsonify({ "status": "error", "message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'" }), 400 # Definir rutas de ejecutables if editor == 'vscode': editor_path = r"C:\Users\migue\AppData\Local\Programs\Microsoft VS Code\Code.exe" editor_name = "VS Code" elif editor == 'cursor': # Rutas comunes donde se instala Cursor possible_cursor_paths = [ r"C:\Users\migue\AppData\Local\Programs\cursor\Cursor.exe", r"C:\Program Files\Cursor\Cursor.exe", r"C:\Program Files (x86)\Cursor\Cursor.exe" ] editor_path = None for path in possible_cursor_paths: if os.path.isfile(path): editor_path = path break if not editor_path: # Intentar buscar en PATH editor_path = shutil.which("cursor") if not editor_path: return jsonify({ "status": "error", "message": f"Cursor no encontrado. Intenté en: {', '.join(possible_cursor_paths)}" }), 404 editor_name = "Cursor" # Verificar que el ejecutable existe if not os.path.isfile(editor_path): return jsonify({ "status": "error", "message": f"{editor_name} no encontrado en: {editor_path}" }), 404 print(f"Launching {editor_name} from: {editor_path}") print(f"Opening directory: {script_group_path}") # Ejecutar el editor process = subprocess.Popen(f'"{editor_path}" "{script_group_path}"', shell=True) print(f"{editor_name} process started with PID: {process.pid}") return jsonify({ "status": "success", "message": f"{editor_name} abierto en: {script_group_path}" }) except Exception as e: print(f"Error opening {editor} for {group_system} group '{group_id}': {str(e)}") return jsonify({ "status": "error", "message": f"Error al abrir {editor}: {str(e)}" }), 500 @app.route("/api/open-group-folder//", methods=["POST"]) def open_group_folder(group_system, group_id): """Abrir carpeta de un grupo en el explorador de archivos""" try: # Determinar directorio según el sistema if group_system == 'config': script_group_path = os.path.join(config_manager.script_groups_path, group_id) if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo config '{group_id}' no encontrado" }), 404 elif group_system == 'launcher': group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({ "status": "error", "message": f"Grupo launcher '{group_id}' no encontrado" }), 404 script_group_path = group["directory"] if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo launcher '{group['name']}' no encontrado" }), 404 else: return jsonify({ "status": "error", "message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'" }), 400 # Abrir en el explorador según el sistema operativo try: if sys.platform == "win32": os.startfile(script_group_path) elif sys.platform == "darwin": # macOS subprocess.Popen(["open", script_group_path]) else: # linux variants subprocess.Popen(["xdg-open", script_group_path]) return jsonify({ "status": "success", "message": f"Abriendo '{script_group_path}' en el explorador", "path": script_group_path }) except Exception as e: return jsonify({ "status": "error", "message": f"Error al abrir el explorador en '{script_group_path}': {str(e)}" }), 500 except Exception as e: print(f"Error opening folder for {group_system} group '{group_id}': {str(e)}") return jsonify({ "status": "error", "message": f"Error al abrir carpeta: {str(e)}" }), 500 @app.route("/api/get-group-path//", methods=["GET"]) def get_group_path(group_system, group_id): """Obtener el path completo de un grupo de scripts""" try: # Determinar directorio según el sistema if group_system == 'config': script_group_path = os.path.join(config_manager.script_groups_path, group_id) if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo config '{group_id}' no encontrado" }), 404 elif group_system == 'launcher': group = launcher_manager.get_launcher_group(group_id) if not group: return jsonify({ "status": "error", "message": f"Grupo launcher '{group_id}' no encontrado" }), 404 script_group_path = group["directory"] if not os.path.isdir(script_group_path): return jsonify({ "status": "error", "message": f"Directorio del grupo launcher '{group['name']}' no encontrado" }), 404 else: return jsonify({ "status": "error", "message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'" }), 400 return jsonify({ "status": "success", "path": script_group_path }) except Exception as e: print(f"Error getting path for {group_system} group '{group_id}': {str(e)}") return jsonify({ "status": "error", "message": f"Error al obtener path: {str(e)}" }), 500 if __name__ == "__main__": # --- Start Flask in a background thread --- flask_thread = threading.Thread(target=run_flask, daemon=True) flask_thread.start() # --- Setup and run the system tray icon --- icon_path = ( r"d:\Proyectos\Scripts\ParamManagerScripts\icon.png" # Use absolute path ) try: image = Image.open(icon_path) menu = pystray.Menu( pystray.MenuItem("Abrir ParamManager", open_app_browser, default=True), pystray.MenuItem("Salir", exit_application), ) tray_icon = pystray.Icon("ParamManager", image, "ParamManager", menu) print("Starting system tray icon...") tray_icon.run() # This blocks the main thread until icon.stop() is called except FileNotFoundError: print( f"Error: Icono no encontrado en '{icon_path}'. El icono de notificación no se iniciará.", file=sys.stderr, ) print( "La aplicación Flask seguirá ejecutándose en segundo plano. Presiona Ctrl+C para detenerla si es necesario." ) # Keep the main thread alive so the Flask thread doesn't exit immediately # This allows Flask to continue running even without the tray icon. try: while flask_thread.is_alive(): flask_thread.join(timeout=1.0) # Wait indefinitely except KeyboardInterrupt: print("\nCtrl+C detectado. Intentando detener Flask...") shutdown_flask_server() # Try to shutdown Flask on Ctrl+C too print("Saliendo.") except Exception as e: print(f"Error al iniciar el icono de notificación: {e}", file=sys.stderr) print("Aplicación finalizada.") # --- Iniciar hilo de vaciado en segundo plano (única vez) --- # flusher_thread = threading.Thread(target=_broadcast_flush_loop, daemon=True) # flusher_thread.start() # ------------------------------------------------------------