#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
editor_sipi_electron.py - IDE de escritorio para SiPi Electron.

Interfaz profesional (no una ventana basica de "pegar y correr"):
editor de codigo con numeros de linea y resaltado de sintaxis propio
de SiPi Electron, una "placa virtual" que muestra en vivo el estado
de cada pin (digital, PWM, lectura analogica) mientras el programa
corre, y una consola con la salida real del interprete en C.

Como funciona la ejecucion: este IDE NO reimplementa el lenguaje.
Guarda el codigo a un archivo .spe temporal (o el que este abierto) y
corre el binario real 'sipi-electron' (compilado con 'make' desde
sipi-electron/) como subproceso, leyendo su salida linea por linea en
un hilo aparte para no congelar la interfaz. Las lineas que empiezan
con "[hal]" (lo que imprime la capa de hardware simulada, ver
src/hal_host.c) se interpretan para animar la placa virtual; el resto
se muestra tal cual en la consola.

Requiere: Python 3.8+, tkinter (viene con Python). Si esta instalado
'customtkinter' se usa para un look mas moderno; si no, cae a tkinter
puro automaticamente -- el IDE funciona en los dos casos.
"""
import os
import re
import sys
import json
import queue
import shutil
import threading
import subprocess
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, font as tkfont, ttk

try:
    import customtkinter as ctk
    ctk.set_appearance_mode("dark")
    _CTK_DISPONIBLE = True
except Exception:
    _CTK_DISPONIBLE = False


# --------------------------------------------------------------------------
# Paleta de colores propia de SiPi Electron.
#
# Se eligio un acento cobre/ambar (#fab387) para toda la marca de
# Electron -- distinto del violeta de marca del SiPi "grande"
# (#8839ef, usado en el logo de editor_sipi.py) y del indigo de su
# boton de compilar (#5e5ce6), para que ningun logo ni boton comparta
# color entre los dos editores ni dentro del mismo.
# --------------------------------------------------------------------------
COLORES = {
    "fondo": "#1e1e2e",
    "fondo_alt": "#181825",
    "barra": "#11111b",
    "panel": "#181825",
    "borde": "#313244",
    "texto": "#e6e6f0",
    "texto_tenue": "#a6adc8",
    "acento": "#fab387",        # cobre/ambar -- marca de SiPi Electron
    "acento_hover": "#ffb999",
    "ejecutar": "#40a02b",
    "ejecutar_hover": "#5fc93f",
    "detener": "#d20f39",
    "detener_hover": "#e64553",
    "azul": "#04a5e5",
    "azul_hover": "#3fc1f0",
    "clave": "#89b4fa",
    "cadena": "#a6e3a1",
    "comentario": "#6c7086",
    "numero": "#f9e2af",
    "builtin": "#fab387",
    "pin_apagado": "#3b3b52",
    "pin_encendido": "#f9e2af",
    "pin_pwm": "#fab387",
    "pin_lectura": "#cba6f7",
    "pin_entrada_borde": "#89b4fa",
}

# Paleta clara alternativa -- mismo acento cobre, para quien prefiera
# trabajar con fondo claro. Se activa con el boton "Tema" de la barra
# de herramientas o Ctrl+Shift+T; no reemplaza COLORES (que sigue
# siendo la paleta base/oscura) sino que se aplica en caliente sobre
# los widgets ya creados desde _aplicar_tema().
COLORES_CLARO = {
    "fondo": "#f5f5fa",
    "fondo_alt": "#e8e8f0",
    "barra": "#ffffff",
    "panel": "#eeeef5",
    "borde": "#c7c7d9",
    "texto": "#1e1e2e",
    "texto_tenue": "#5c5c72",
    "acento": "#e6793f",
    "acento_hover": "#f28c55",
    "ejecutar": "#2f8a1c",
    "ejecutar_hover": "#3ba825",
    "detener": "#b3092c",
    "detener_hover": "#d20f39",
    "azul": "#0384ad",
    "azul_hover": "#04a5e5",
    "clave": "#1e5fbf",
    "cadena": "#1a7a3c",
    "comentario": "#8a8fa3",
    "numero": "#a8710b",
    "builtin": "#e6793f",
    "pin_apagado": "#c7c7d9",
    "pin_encendido": "#f9c74f",
    "pin_pwm": "#e6793f",
    "pin_lectura": "#8839ef",
    "pin_entrada_borde": "#1e5fbf",
}

PALABRAS_CLAVE = {
    "programa", "decir", "variable", "const", "si", "sino", "fin",
    "mientras", "repetir", "veces", "romper", "continuar",
    "funcion", "devolver", "llamar", "llamar_valor",
    "verdadero", "falso", "y", "o", "no",
}

FUNCIONES_HARDWARE = {
    "pin_modo", "pin_escribir", "pin_leer", "analogico_leer",
    "pwm_escribir", "esperar_ms", "esperar_segundos",
    "raiz_cuadrada", "azar_entre", "redondear", "absoluto",
}

EJEMPLO_INICIAL = '''programa "Mi primer programa"

variable pin_led = 13
pin_modo(pin_led, 1)

repetir 5 veces
    pin_escribir(pin_led, 1)
    esperar_ms(300)
    pin_escribir(pin_led, 0)
    esperar_ms(300)
fin

decir "Listo!"
'''

N_PINES_PLACA = 32  # tope por defecto (placa "Generica")

# ----------------------------------------------------------------------
# Presets de placa: cuantos pines simular y cuales marcar como analogicos
# (se dibujan con una etiqueta "A" en vez del numero crudo). El HAL en C
# sigue siendo host-simulado para cualquier numero de pin sin importar el
# preset elegido -- esto es solo para que la placa virtual se vea y se
# etiquete como la placa real que el usuario tiene en mente.
# ----------------------------------------------------------------------
PLACAS = {
    "Generica (32 pines)": {"pines": 32, "analogicos": set()},
    "Arduino Uno/Nano (14 D + 6 A)": {"pines": 20, "analogicos": {14, 15, 16, 17, 18, 19}},
    "Arduino Mega (54 D + 16 A)": {"pines": 70, "analogicos": set(range(54, 70))},
    "ESP32 DevKit (34 GPIO)": {"pines": 34, "analogicos": set(range(32, 34))},
}

# Snippets listos para insertar desde el menu "Insertar" -- cubren los
# patrones de hardware mas comunes para no obligar a escribirlos de cero
# ni a ir a buscarlos en examples/ cada vez.
SNIPPETS = {
    "Parpadeo de LED": '''variable pin_led = 13
pin_modo(pin_led, 1)

repetir 10 veces
    pin_escribir(pin_led, 1)
    esperar_ms(500)
    pin_escribir(pin_led, 0)
    esperar_ms(500)
fin
''',
    "Leer sensor analogico": '''variable pin_sensor = 14
pin_modo(pin_sensor, 0)

mientras verdadero
    variable lectura = analogico_leer(pin_sensor)
    decir "Lectura: " + lectura
    esperar_ms(200)
fin
''',
    "Boton de entrada digital": '''variable pin_boton = 2
variable pin_led = 13
pin_modo(pin_boton, 0)
pin_modo(pin_led, 1)

mientras verdadero
    variable presionado = pin_leer(pin_boton)
    si presionado == 1
        pin_escribir(pin_led, 1)
    sino
        pin_escribir(pin_led, 0)
    fin
    esperar_ms(20)
fin
''',
    "Salida PWM (brillo/velocidad)": '''variable pin_pwm = 9
pin_modo(pin_pwm, 1)

variable nivel = 0
mientras nivel <= 255
    pwm_escribir(pin_pwm, nivel)
    esperar_ms(20)
    nivel = nivel + 5
fin
''',
    "Funcion reutilizable": '''funcion parpadear(pin, veces_repetir)
    variable i = 0
    mientras i < veces_repetir
        pin_escribir(pin, 1)
        esperar_ms(200)
        pin_escribir(pin, 0)
        esperar_ms(200)
        i = i + 1
    fin
fin

pin_modo(13, 1)
llamar parpadear(13, 5)
''',
}

RUTA_CONFIG = os.path.join(os.path.expanduser("~"), ".sipi_electron_ide.json")


def _cargar_config():
    try:
        with open(RUTA_CONFIG, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return {}


def _guardar_config(datos):
    try:
        with open(RUTA_CONFIG, "w", encoding="utf-8") as f:
            json.dump(datos, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


def _raiz_proyecto():
    """Carpeta raiz de sipi-electron (un nivel arriba de ide/)."""
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def _ruta_binario():
    nombre = "sipi-electron.exe" if os.name == "nt" else "sipi-electron"
    return os.path.join(_raiz_proyecto(), nombre)


class EditorSipiElectron:
    def __init__(self, root):
        self.root = root
        self.config = _cargar_config()

        self.root.title("SiPi Electron")
        self.root.geometry(self.config.get("geometria", "1280x820"))
        self.root.configure(bg=COLORES["fondo"])

        self.ruta_archivo = None
        self.modificado = False
        self.proceso = None
        self.hilo_lectura = None
        self.cola_salida = queue.Queue()
        self.pin_ovalos = {}
        self.pin_etiquetas = {}
        self.pin_estado = {}  # pin -> {"modo": int|None, "valor": int, "digital": int|None}
        self.tamano_fuente = self.config.get("tamano_fuente", 12)
        self.nombre_placa = self.config.get("placa", "Generica (32 pines)")
        if self.nombre_placa not in PLACAS:
            self.nombre_placa = "Generica (32 pines)"
        self.recientes = [r for r in self.config.get("recientes", []) if os.path.isfile(r)][:8]
        self.linea_error_actual = None
        self.tema = self.config.get("tema", "oscuro")
        self.autoindentar = self.config.get("autoindentar", True)
        self.ajuste_linea = self.config.get("ajuste_linea", False)

        self._crear_icono_ventana()
        self._crear_menu()
        self._crear_barra_herramientas()
        self._crear_cuerpo()
        self._crear_barra_estado()
        self._fijar_estado_botones_ejecucion(corriendo=False)

        self._cargar_texto(EJEMPLO_INICIAL)
        self._resaltar_sintaxis()
        self._actualizar_titulo()
        self._atajos_teclado()
        self._aplicar_tamano_fuente()
        self._aplicar_ajuste_linea()
        if self.tema == "claro":
            self._aplicar_tema()

        self.root.protocol("WM_DELETE_WINDOW", self._al_cerrar)
        self.root.after(40, self._procesar_cola_salida)

    # ------------------------------------------------------------------
    # Icono de la ventana: circuito estilizado en el color de marca de
    # Electron (#fab387), distinto a proposito del logo violeta del
    # SiPi "grande" para que nunca se confundan ni se "pisen" si algun
    # dia se muestran juntos (por ejemplo en un selector de proyectos).
    # ------------------------------------------------------------------
    def _crear_icono_ventana(self):
        """Dibuja el icono de la ventana a mano, pixel a pixel, con
        Tkinter puro (sin depender de un .png/.ico externo ni de datos
        base64 pre-generados): un circulo cobre con un "pin" adentro,
        en el color de marca de Electron (#fab387) -- nunca el mismo
        violeta que usa el logo del SiPi grande."""
        try:
            tam = 32
            img = tk.PhotoImage(width=tam, height=tam)
            transparente = "#1e1e2e"
            img.put(transparente, to=(0, 0, tam, tam))
            cx, cy, r = tam / 2, tam / 2, tam / 2 - 2
            for y in range(tam):
                for x in range(tam):
                    d = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
                    if d <= r:
                        img.put(COLORES["acento"], (x, y))
            # "pin" central mas oscuro para dar textura de circuito
            for y in range(int(cy - 3), int(cy + 3)):
                for x in range(int(cx - 1), int(cx + 1)):
                    img.put(COLORES["fondo_alt"], (x, y))
            self.root.iconphoto(True, img)
            self._icono_ref = img
        except Exception:
            pass

    # ------------------------------------------------------------------
    # Barra de menu (Archivo/Editar/Ejecutar/Ver/Placa/Ayuda) -- ademas
    # de la barra de botones grandes, para que las acciones menos usadas
    # (buscar/reemplazar, ir a linea, insertar snippet, elegir placa,
    # archivos recientes) tengan un lugar fijo sin abarrotar la barra
    # principal de iconos.
    # ------------------------------------------------------------------
    def _crear_menu(self):
        barra_menu = tk.Menu(self.root)
        self.root.config(menu=barra_menu)

        m_archivo = tk.Menu(barra_menu, tearoff=0)
        m_archivo.add_command(label="Nuevo", accelerator="Ctrl+N", command=self.nuevo)
        m_archivo.add_command(label="Abrir...", accelerator="Ctrl+O", command=self.abrir)
        m_archivo.add_command(label="Guardar", accelerator="Ctrl+S", command=self.guardar)
        m_archivo.add_command(label="Guardar como...", accelerator="Ctrl+Shift+S", command=self.guardar_como)
        m_archivo.add_separator()
        self.menu_recientes = tk.Menu(m_archivo, tearoff=0)
        m_archivo.add_cascade(label="Abrir reciente", menu=self.menu_recientes)
        self._reconstruir_menu_recientes()
        m_archivo.add_separator()
        m_archivo.add_command(label="Salir", command=self._al_cerrar)
        barra_menu.add_cascade(label="Archivo", menu=m_archivo)

        m_editar = tk.Menu(barra_menu, tearoff=0)
        m_editar.add_command(label="Deshacer", accelerator="Ctrl+Z", command=lambda: self.texto.edit_undo())
        m_editar.add_command(label="Rehacer", accelerator="Ctrl+Y", command=lambda: self.texto.edit_redo())
        m_editar.add_separator()
        m_editar.add_command(label="Cortar", accelerator="Ctrl+X", command=lambda: self.texto.event_generate("<<Cut>>"))
        m_editar.add_command(label="Copiar", accelerator="Ctrl+C", command=lambda: self.texto.event_generate("<<Copy>>"))
        m_editar.add_command(label="Pegar", accelerator="Ctrl+V", command=lambda: self.texto.event_generate("<<Paste>>"))
        m_editar.add_command(label="Seleccionar todo", accelerator="Ctrl+A", command=self.seleccionar_todo)
        m_editar.add_separator()
        m_editar.add_command(label="Comentar / descomentar linea", accelerator="Ctrl+/", command=self.comentar_descomentar)
        m_editar.add_command(label="Duplicar linea", accelerator="Ctrl+D", command=self.duplicar_linea)
        m_editar.add_command(label="Eliminar linea", accelerator="Ctrl+Shift+K", command=self.eliminar_linea)
        m_editar.add_command(label="Mover linea arriba", accelerator="Alt+Up", command=lambda: self.mover_linea(-1))
        m_editar.add_command(label="Mover linea abajo", accelerator="Alt+Down", command=lambda: self.mover_linea(1))
        m_editar.add_separator()
        m_editar.add_command(label="Buscar...", accelerator="Ctrl+F", command=self.abrir_buscar)
        m_editar.add_command(label="Buscar y reemplazar...", accelerator="Ctrl+H", command=self.abrir_buscar_reemplazar)
        m_editar.add_command(label="Ir a linea...", accelerator="Ctrl+G", command=self.ir_a_linea)
        barra_menu.add_cascade(label="Editar", menu=m_editar)

        m_insertar = tk.Menu(barra_menu, tearoff=0)
        for nombre in SNIPPETS:
            m_insertar.add_command(label=nombre, command=lambda n=nombre: self.insertar_snippet(n))
        barra_menu.add_cascade(label="Insertar", menu=m_insertar)

        m_ejecutar = tk.Menu(barra_menu, tearoff=0)
        m_ejecutar.add_command(label="Ejecutar", accelerator="F5", command=self.ejecutar)
        m_ejecutar.add_command(label="Detener", accelerator="Shift+F5", command=self.detener)
        m_ejecutar.add_command(label="Recompilar motor", command=self.recompilar_motor)
        m_ejecutar.add_separator()
        m_ejecutar.add_command(label="Limpiar consola", command=self.limpiar_consola)
        m_ejecutar.add_command(label="Exportar consola a archivo...", command=self.exportar_consola)
        barra_menu.add_cascade(label="Ejecutar", menu=m_ejecutar)

        m_ver = tk.Menu(barra_menu, tearoff=0)
        m_ver.add_command(label="Acercar texto", accelerator="Ctrl++", command=lambda: self.cambiar_zoom(1))
        m_ver.add_command(label="Alejar texto", accelerator="Ctrl+-", command=lambda: self.cambiar_zoom(-1))
        m_ver.add_command(label="Restablecer zoom", accelerator="Ctrl+0", command=lambda: self.cambiar_zoom(0))
        m_ver.add_separator()
        m_ver.add_command(label="Alternar tema claro/oscuro", accelerator="Ctrl+Shift+T", command=self.alternar_tema)
        self.var_ajuste = tk.BooleanVar(value=self.ajuste_linea)
        m_ver.add_checkbutton(label="Ajuste de linea", variable=self.var_ajuste, command=self._alternar_ajuste_linea)
        self.var_autoindentar = tk.BooleanVar(value=self.autoindentar)
        m_ver.add_checkbutton(label="Auto-indentar", variable=self.var_autoindentar, command=self._alternar_autoindentar)
        barra_menu.add_cascade(label="Ver", menu=m_ver)

        m_placa = tk.Menu(barra_menu, tearoff=0)
        self.var_placa = tk.StringVar(value=self.nombre_placa)
        for nombre_placa in PLACAS:
            m_placa.add_radiobutton(label=nombre_placa, variable=self.var_placa, value=nombre_placa,
                                     command=lambda n=nombre_placa: self.elegir_placa(n))
        barra_menu.add_cascade(label="Placa", menu=m_placa)

        m_ayuda = tk.Menu(barra_menu, tearoff=0)
        m_ayuda.add_command(label="Ver ejemplos...", command=self.abrir_ejemplos)
        m_ayuda.add_command(label="Acerca de SiPi Electron", command=self.mostrar_acerca_de)
        barra_menu.add_cascade(label="Ayuda", menu=m_ayuda)

    def mostrar_acerca_de(self):
        messagebox.showinfo(
            "Acerca de SiPi Electron",
            "SiPi Electron\n\n"
            "Variante de SiPi en C pensada para electronica, robots y "
            "microcontroladores. Este IDE corre el interprete real como "
            "subproceso y anima una placa virtual leyendo su salida.\n\n"
            "Parte del ecosistema SiPi (NovaLab)."
        )

    # ------------------------------------------------------------------
    # Barra de herramientas
    # ------------------------------------------------------------------
    def _agregar_hover(self, widget, color_normal, color_hover):
        """Mismo motivo que en editor_sipi.py: tk.Button no muestra
        feedback al pasar el mouse por encima (solo al hacer clic), asi
        que sin esto los botones se sienten menos responsivos que la
        version con CustomTkinter."""
        def _entrar(evento=None):
            try:
                if str(widget.cget("state")) == "disabled":
                    return
                widget.configure(bg=color_hover)
            except Exception:
                pass

        def _salir(evento=None):
            try:
                widget.configure(bg=color_normal)
            except Exception:
                pass

        widget.bind("<Enter>", _entrar, add="+")
        widget.bind("<Leave>", _salir, add="+")

    def _boton(self, contenedor, texto, comando, bg, hover, ancho=110, negrita=False):
        if _CTK_DISPONIBLE:
            fuente = ctk.CTkFont(family="Segoe UI", size=13, weight="bold" if negrita else "normal")
            b = ctk.CTkButton(contenedor, text=texto, command=comando, fg_color=bg,
                               hover_color=hover, text_color="white", font=fuente,
                               corner_radius=6, width=ancho, height=32, cursor="hand2")
        else:
            b = tk.Button(contenedor, text=texto, command=comando, bg=bg, fg="white",
                           activebackground=hover, activeforeground="white", bd=0,
                           font=("Segoe UI", 10, "bold" if negrita else "normal"),
                           padx=12, pady=6, cursor="hand2", width=max(8, ancho // 10))
            self._agregar_hover(b, bg, hover)
        b.pack(side=tk.LEFT, padx=4, pady=6)
        return b

    # ------------------------------------------------------------------
    # Iconos vectoriales propios (dibujados a mano con Canvas, igual que
    # el icono de la ventana): nada de emojis ni de imagenes externas,
    # asi el look es consistente en cualquier sistema operativo/fuente.
    # ------------------------------------------------------------------
    def _dibujar_icono(self, canvas, nombre, color):
        t = 15  # lienzo logico 0..t
        if nombre == "play":
            canvas.create_polygon(4, 2, 4, t - 2, t - 2, t / 2, fill=color, outline="")
        elif nombre == "stop":
            canvas.create_rectangle(3, 3, t - 3, t - 3, fill=color, outline="")
        elif nombre == "reload":
            canvas.create_arc(2, 2, t - 2, t - 2, start=40, extent=270, style="arc",
                               outline=color, width=2)
            canvas.create_polygon(t - 5, 1, t - 1, 2, t - 3, 5, fill=color, outline="")
        elif nombre == "tema":
            canvas.create_oval(2, 2, t - 2, t - 2, outline=color, width=2)
            canvas.create_arc(2, 2, t - 2, t - 2, start=90, extent=180, fill=color, outline="")
        elif nombre == "undo":
            canvas.create_line(t - 3, 3, 4, 3, fill=color, width=2)
            canvas.create_line(4, 3, 4, t - 3, fill=color, width=2)
            canvas.create_polygon(2, 3, 6, 0, 6, 6, fill=color, outline="")
        elif nombre == "redo":
            canvas.create_line(3, 3, t - 4, 3, fill=color, width=2)
            canvas.create_line(t - 4, 3, t - 4, t - 3, fill=color, width=2)
            canvas.create_polygon(t - 2, 3, t - 6, 0, t - 6, 6, fill=color, outline="")
        elif nombre == "duplicar":
            canvas.create_rectangle(2, 4, t - 5, t - 2, outline=color, width=2)
            canvas.create_rectangle(6, 1, t - 1, t - 6, fill=COLORES["fondo"], outline=color, width=2)
        elif nombre == "buscar":
            canvas.create_oval(2, 2, t - 5, t - 5, outline=color, width=2)
            canvas.create_line(t - 6, t - 6, t - 1, t - 1, fill=color, width=2)
        elif nombre == "flecha_arriba":
            canvas.create_polygon(t / 2, 2, t - 3, t - 3, 3, t - 3, fill=color, outline="")
        elif nombre == "flecha_abajo":
            canvas.create_polygon(t / 2, t - 2, t - 3, 3, 3, 3, fill=color, outline="")
        elif nombre == "papelera":
            canvas.create_rectangle(3, 4, t - 3, t - 1, outline=color, width=2)
            canvas.create_line(1, 4, t - 1, 4, fill=color, width=2)
            canvas.create_rectangle(5, 1, t - 5, 4, outline=color, width=2)
        elif nombre == "zoom_mas":
            canvas.create_oval(2, 2, t - 5, t - 5, outline=color, width=2)
            canvas.create_line(t - 6, t - 6, t - 1, t - 1, fill=color, width=2)
            canvas.create_line(3, 6, 8, 6, fill=color, width=1)
            canvas.create_line(5.5, 3.5, 5.5, 8.5, fill=color, width=1)
        elif nombre == "zoom_menos":
            canvas.create_oval(2, 2, t - 5, t - 5, outline=color, width=2)
            canvas.create_line(t - 6, t - 6, t - 1, t - 1, fill=color, width=2)
            canvas.create_line(3, 6, 8, 6, fill=color, width=1)
        elif nombre == "carpeta":
            canvas.create_polygon(1, 4, 6, 4, 8, 6, t - 1, 6, t - 1, t - 2, 1, t - 2, fill=color, outline="")
        elif nombre == "nuevo":
            canvas.create_rectangle(3, 1, t - 3, t - 1, outline=color, width=2)
            canvas.create_line(5, 5, t - 5, 5, fill=color, width=1)
            canvas.create_line(5, 8, t - 5, 8, fill=color, width=1)
        elif nombre == "guardar":
            canvas.create_rectangle(2, 2, t - 2, t - 2, outline=color, width=2)
            canvas.create_rectangle(5, 2, t - 5, 6, fill=color, outline="")

    def _boton_icono(self, contenedor, icono, texto, comando, bg, hover, ancho=110, negrita=False):
        """Boton con icono vectorial propio (sin emojis) + texto, con el
        mismo look/hover que _boton pero dibujado a mano con Canvas."""
        marco = tk.Frame(contenedor, bg=bg, cursor="hand2")
        marco.pack(side=tk.LEFT, padx=4, pady=6)

        lienzo = tk.Canvas(marco, width=16, height=16, bg=bg, highlightthickness=0, cursor="hand2")
        lienzo.pack(side=tk.LEFT, padx=(10, 4), pady=6)
        self._dibujar_icono(lienzo, icono, "white")

        etiqueta = tk.Label(marco, text=texto, bg=bg, fg="white", cursor="hand2",
                             font=("Segoe UI", 10, "bold" if negrita else "normal"))
        etiqueta.pack(side=tk.LEFT, padx=(0, 10), pady=6)

        def _clic(evento=None):
            comando()

        def _entrar(evento=None):
            marco.configure(bg=hover)
            lienzo.configure(bg=hover)
            etiqueta.configure(bg=hover)

        def _salir(evento=None):
            marco.configure(bg=bg)
            lienzo.configure(bg=bg)
            etiqueta.configure(bg=bg)

        for w in (marco, lienzo, etiqueta):
            w.bind("<Button-1>", _clic)
            w.bind("<Enter>", _entrar)
            w.bind("<Leave>", _salir)
        return marco

    def _separador(self, contenedor):
        if _CTK_DISPONIBLE:
            ctk.CTkFrame(contenedor, fg_color=COLORES["borde"], width=1, corner_radius=0).pack(
                side=tk.LEFT, fill=tk.Y, padx=10, pady=8)
        else:
            tk.Frame(contenedor, bg=COLORES["borde"], width=1).pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=8)

    def _crear_barra_herramientas(self):
        if _CTK_DISPONIBLE:
            self.barra = ctk.CTkFrame(self.root, fg_color=COLORES["barra"], height=52, corner_radius=0)
        else:
            self.barra = tk.Frame(self.root, bg=COLORES["barra"], height=52)
        self.barra.pack(side=tk.TOP, fill=tk.X)

        marca = tk.Frame(self.barra, bg=COLORES["barra"])
        marca.pack(side=tk.LEFT, padx=(14, 6))
        tk.Canvas(marca, width=22, height=22, bg=COLORES["barra"], highlightthickness=0
                  ).create_oval(2, 2, 20, 20, fill=COLORES["acento"], outline="")
        cv = tk.Canvas(marca, width=22, height=22, bg=COLORES["barra"], highlightthickness=0)
        cv.pack(side=tk.LEFT)
        cv.create_oval(3, 3, 19, 19, outline=COLORES["acento"], width=2)
        cv.create_line(11, 3, 11, 0, fill=COLORES["acento"], width=2)
        cv.create_oval(9, 9, 13, 13, fill=COLORES["acento"], outline="")
        tk.Label(marca, text="SiPi Electron", bg=COLORES["barra"], fg=COLORES["acento"],
                 font=("Segoe UI", 14, "bold")).pack(side=tk.LEFT, padx=8)

        self._separador(self.barra)
        self._boton_icono(self.barra, "nuevo", "Nuevo", self.nuevo, COLORES["borde"], "#45475a", 90)
        self._boton_icono(self.barra, "carpeta", "Abrir", self.abrir, COLORES["borde"], "#45475a", 90)
        self._boton_icono(self.barra, "guardar", "Guardar", self.guardar, COLORES["borde"], "#45475a", 100)
        self._boton(self.barra, "Guardar como", self.guardar_como, COLORES["borde"], "#45475a", 130)

        self._separador(self.barra)
        self.boton_ejecutar = self._boton_icono(self.barra, "play", "Ejecutar", self.ejecutar,
                                                  COLORES["ejecutar"], COLORES["ejecutar_hover"], 110, True)
        self.boton_detener = self._boton_icono(self.barra, "stop", "Detener", self.detener,
                                                COLORES["detener"], COLORES["detener_hover"], 100, True)
        self._boton_icono(self.barra, "reload", "Recompilar", self.recompilar_motor, COLORES["azul"], COLORES["azul_hover"], 120)
        self._boton(self.barra, "Limpiar consola", self.limpiar_consola, COLORES["borde"], "#45475a", 140)

        self._separador(self.barra)
        self._boton(self.barra, "Ejemplos", self.abrir_ejemplos, COLORES["azul"], COLORES["azul_hover"], 100)
        self._boton_icono(self.barra, "tema", "Tema", self.alternar_tema, COLORES["borde"], "#45475a", 90)

        # ---- segunda fila: edicion rapida, snippets, placa, zoom ----
        if _CTK_DISPONIBLE:
            self.barra2 = ctk.CTkFrame(self.root, fg_color=COLORES["fondo_alt"], height=44, corner_radius=0)
        else:
            self.barra2 = tk.Frame(self.root, bg=COLORES["fondo_alt"], height=44)
        self.barra2.pack(side=tk.TOP, fill=tk.X)

        self._boton_icono(self.barra2, "undo", "Deshacer", lambda: self.texto.edit_undo(), COLORES["borde"], "#45475a", 100)
        self._boton_icono(self.barra2, "redo", "Rehacer", lambda: self.texto.edit_redo(), COLORES["borde"], "#45475a", 100)
        self._separador(self.barra2)
        self._boton(self.barra2, "// Comentar", self.comentar_descomentar, COLORES["borde"], "#45475a", 110)
        self._boton_icono(self.barra2, "duplicar", "Duplicar", self.duplicar_linea, COLORES["borde"], "#45475a", 100)
        self._boton_icono(self.barra2, "flecha_arriba", "", lambda: self.mover_linea(-1), COLORES["borde"], "#45475a", 30)
        self._boton_icono(self.barra2, "flecha_abajo", "", lambda: self.mover_linea(1), COLORES["borde"], "#45475a", 30)
        self._separador(self.barra2)
        self._boton_icono(self.barra2, "buscar", "Buscar", self.abrir_buscar, COLORES["borde"], "#45475a", 100)
        self._boton(self.barra2, "Reemplazar", self.abrir_buscar_reemplazar, COLORES["borde"], "#45475a", 110)
        self._boton(self.barra2, "Ir a linea", self.ir_a_linea, COLORES["borde"], "#45475a", 100)
        self._separador(self.barra2)
        self._boton_icono(self.barra2, "zoom_menos", "", lambda: self.cambiar_zoom(-1), COLORES["borde"], "#45475a", 30)
        self._boton_icono(self.barra2, "zoom_mas", "", lambda: self.cambiar_zoom(1), COLORES["borde"], "#45475a", 30)

        self._separador(self.barra2)
        tk.Label(self.barra2, text="Snippet:", bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                 font=("Segoe UI", 9)).pack(side=tk.LEFT, padx=(4, 4))
        self.var_snippet = tk.StringVar(value="Insertar...")
        combo_snippet = ttk.Combobox(self.barra2, textvariable=self.var_snippet, values=list(SNIPPETS.keys()),
                                      state="readonly", width=22)
        combo_snippet.pack(side=tk.LEFT, padx=4, pady=6)
        combo_snippet.bind("<<ComboboxSelected>>", lambda e: (self.insertar_snippet(self.var_snippet.get()),
                                                                self.var_snippet.set("Insertar...")))

        tk.Label(self.barra2, text="Placa:", bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                 font=("Segoe UI", 9)).pack(side=tk.LEFT, padx=(10, 4))
        combo_placa = ttk.Combobox(self.barra2, textvariable=self.var_placa if hasattr(self, "var_placa") else tk.StringVar(),
                                    values=list(PLACAS.keys()), state="readonly", width=22)
        combo_placa.pack(side=tk.LEFT, padx=4, pady=6)
        combo_placa.bind("<<ComboboxSelected>>", lambda e: self.elegir_placa(combo_placa.get()))
        self.combo_placa = combo_placa

    # ------------------------------------------------------------------
    # Cuerpo: editor a la izquierda, placa virtual + consola a la derecha
    # ------------------------------------------------------------------
    def _crear_cuerpo(self):
        self.paned = tk.PanedWindow(self.root, orient=tk.HORIZONTAL, bg=COLORES["fondo"],
                                     sashwidth=6, bd=0, sashrelief=tk.FLAT)
        self.paned.pack(fill=tk.BOTH, expand=True)

        # ---- panel izquierdo: editor de codigo ----
        panel_editor = tk.Frame(self.paned, bg=COLORES["fondo"])
        tk.Label(panel_editor, text="  CODIGO  (.spe)", bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                 font=("Segoe UI", 9, "bold"), anchor="w").pack(fill=tk.X)

        marco_editor = tk.Frame(panel_editor, bg=COLORES["fondo"])
        marco_editor.pack(fill=tk.BOTH, expand=True)

        self.numeros = tk.Text(marco_editor, width=4, bg=COLORES["fondo_alt"], fg="#6c7086",
                                bd=0, font=("Consolas", 12), state="disabled", takefocus=0,
                                cursor="arrow")
        self.numeros.pack(side=tk.LEFT, fill=tk.Y)

        self.texto = tk.Text(marco_editor, bg=COLORES["fondo"], fg=COLORES["texto"], bd=0,
                              insertbackground=COLORES["acento"], font=("Consolas", 12),
                              undo=True, wrap="none", padx=8, selectbackground="#45475a")
        self.texto.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.texto.bind("<KeyRelease>", self._al_escribir)
        self.texto.bind("<<Modified>>", self._al_modificar)
        self.texto.bind("<Return>", self._al_presionar_enter)

        scroll_y = tk.Scrollbar(marco_editor, command=self._scroll_conjunto)
        scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
        self.texto.configure(yscrollcommand=lambda a, b: self._sync_scroll(a, b, scroll_y))
        self.numeros.configure(yscrollcommand=lambda a, b: None)

        self.texto.tag_configure("clave", foreground=COLORES["clave"])
        self.texto.tag_configure("cadena", foreground=COLORES["cadena"])
        self.texto.tag_configure("comentario", foreground=COLORES["comentario"])
        self.texto.tag_configure("numero", foreground=COLORES["numero"])
        self.texto.tag_configure("hardware", foreground=COLORES["builtin"])
        self.texto.tag_configure("linea_error", background="#3a1d24", underline=True)
        self.texto.bind("<KeyRelease>", self._actualizar_indicador_cursor, add="+")
        self.texto.bind("<ButtonRelease-1>", self._actualizar_indicador_cursor, add="+")

        self.paned.add(panel_editor, minsize=420)

        # ---- panel derecho: placa virtual (arriba) + consola (abajo) ----
        panel_derecho = tk.PanedWindow(self.paned, orient=tk.VERTICAL, bg=COLORES["fondo"],
                                        sashwidth=6, bd=0, sashrelief=tk.FLAT)
        self.paned.add(panel_derecho, minsize=420)

        panel_placa = tk.Frame(panel_derecho, bg=COLORES["panel"])
        self._crear_placa_virtual(panel_placa)
        panel_derecho.add(panel_placa, minsize=200)

        panel_consola = tk.Frame(panel_derecho, bg=COLORES["fondo_alt"])
        self._crear_consola(panel_consola)
        panel_derecho.add(panel_consola, minsize=180)

    def _scroll_conjunto(self, *args):
        self.texto.yview(*args)
        self.numeros.yview(*args)

    def _sync_scroll(self, a, b, scrollbar):
        scrollbar.set(a, b)
        self.numeros.yview_moveto(a)

    # ------------------------------------------------------------------
    # Placa virtual: representacion en vivo del estado de los pines
    # ------------------------------------------------------------------
    def _crear_placa_virtual(self, contenedor):
        encabezado = tk.Frame(contenedor, bg=COLORES["fondo_alt"])
        encabezado.pack(fill=tk.X)
        tk.Label(encabezado, text="  PLACA VIRTUAL", bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                 font=("Segoe UI", 9, "bold"), anchor="w").pack(side=tk.LEFT, fill=tk.X, expand=True)

        leyenda = tk.Frame(encabezado, bg=COLORES["fondo_alt"])
        leyenda.pack(side=tk.RIGHT, padx=10, pady=4)
        for color, texto in [
            (COLORES["pin_apagado"], "sin usar"),
            (COLORES["pin_encendido"], "digital ALTO"),
            (COLORES["pin_pwm"], "PWM"),
            (COLORES["pin_lectura"], "lectura"),
        ]:
            marco = tk.Frame(leyenda, bg=COLORES["fondo_alt"])
            marco.pack(side=tk.LEFT, padx=6)
            tk.Canvas(marco, width=10, height=10, bg=COLORES["fondo_alt"], highlightthickness=0
                      ).create_oval(1, 1, 9, 9, fill=color, outline="")
            tk.Label(marco, text=texto, bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                     font=("Segoe UI", 8)).pack(side=tk.LEFT, padx=3)

        self.canvas_placa = tk.Canvas(contenedor, bg=COLORES["panel"], highlightthickness=0)
        self.canvas_placa.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
        self.canvas_placa.bind("<Configure>", lambda e: self._dibujar_placa())
        self._dibujar_placa()

    def _dibujar_placa(self):
        c = self.canvas_placa
        c.delete("all")
        self.pin_ovalos.clear()
        self.pin_etiquetas.clear()

        preset = PLACAS.get(self.nombre_placa, PLACAS["Generica (32 pines)"])
        n_pines = preset["pines"]
        analogicos = preset["analogicos"]

        ancho = max(c.winfo_width(), 400)
        cols = 8
        filas = (n_pines + cols - 1) // cols
        margen = 30
        paso_x = (ancho - 2 * margen) / cols
        paso_y = 64
        radio = 16

        c.create_rectangle(4, 4, ancho - 4, filas * paso_y + 20, outline=COLORES["borde"],
                            width=2, fill=COLORES["fondo_alt"])
        c.create_text(ancho / 2, 16, text=f"SiPi Electron — {self.nombre_placa}", fill=COLORES["texto_tenue"],
                       font=("Segoe UI", 9, "italic"))

        for pin in range(n_pines):
            fila = pin // cols
            col = pin % cols
            x = margen + col * paso_x + paso_x / 2
            y = 46 + fila * paso_y
            estado = self.pin_estado.get(pin, {})
            color = self._color_para_pin(estado)
            borde = COLORES["pin_entrada_borde"] if estado.get("modo") == 0 else COLORES["borde"]
            ovalo = c.create_oval(x - radio, y - radio, x + radio, y + radio,
                                   fill=color, outline=borde, width=2)
            texto_pin = f"A{pin}" if pin in analogicos else str(pin)
            etiqueta = c.create_text(x, y + radio + 12, text=texto_pin, fill=COLORES["texto_tenue"],
                                      font=("Consolas", 9))
            self.pin_ovalos[pin] = ovalo
            self.pin_etiquetas[pin] = etiqueta

    @staticmethod
    def _color_para_pin(estado):
        if not estado:
            return COLORES["pin_apagado"]
        if estado.get("pwm") is not None:
            # interpola entre apagado y el color PWM segun el duty cycle
            return COLORES["pin_pwm"] if estado["pwm"] > 0 else COLORES["pin_apagado"]
        if estado.get("digital") == 1:
            return COLORES["pin_encendido"]
        return COLORES["pin_apagado"]

    def _actualizar_pin(self, pin, **cambios):
        n_pines = PLACAS.get(self.nombre_placa, PLACAS["Generica (32 pines)"])["pines"]
        if pin < 0 or pin >= n_pines:
            return
        estado = self.pin_estado.setdefault(pin, {})
        estado.update(cambios)
        if pin in self.pin_ovalos:
            color = self._color_para_pin(estado)
            borde = COLORES["pin_entrada_borde"] if estado.get("modo") == 0 else COLORES["borde"]
            self.canvas_placa.itemconfig(self.pin_ovalos[pin], fill=color, outline=borde)

    def _destello_lectura(self, pin):
        """Destello violeta breve para marcar una lectura de pin (analogica
        o digital), que no deja un estado permanente como escribir un pin."""
        if pin not in self.pin_ovalos:
            return
        anterior = self._color_para_pin(self.pin_estado.get(pin, {}))
        self.canvas_placa.itemconfig(self.pin_ovalos[pin], fill=COLORES["pin_lectura"])
        self.root.after(180, lambda: self.canvas_placa.itemconfig(self.pin_ovalos[pin], fill=anterior))

    # ------------------------------------------------------------------
    # Consola
    # ------------------------------------------------------------------
    def _crear_consola(self, contenedor):
        encabezado = tk.Frame(contenedor, bg=COLORES["fondo_alt"])
        encabezado.pack(fill=tk.X)
        tk.Label(encabezado, text="  CONSOLA", bg=COLORES["fondo_alt"], fg=COLORES["texto_tenue"],
                 font=("Segoe UI", 9, "bold"), anchor="w").pack(side=tk.LEFT, fill=tk.X, expand=True)
        self.indicador_estado = tk.Label(encabezado, text="● Detenido", bg=COLORES["fondo_alt"],
                                          fg=COLORES["texto_tenue"], font=("Segoe UI", 9, "bold"))
        self.indicador_estado.pack(side=tk.RIGHT, padx=10)

        self.consola = tk.Text(contenedor, bg="#11111b", fg=COLORES["texto"], bd=0,
                                font=("Consolas", 11), state="disabled", wrap="word", padx=8, pady=6)
        self.consola.pack(fill=tk.BOTH, expand=True)
        self.consola.tag_configure("hal", foreground=COLORES["acento"])
        self.consola.tag_configure("error", foreground=COLORES["detener"])
        self.consola.tag_configure("info", foreground=COLORES["texto_tenue"])
        self.consola.tag_configure("salida", foreground=COLORES["texto"])

    def _log(self, texto, etiqueta="salida"):
        self.consola.configure(state="normal")
        self.consola.insert(tk.END, texto + "\n", etiqueta)
        self.consola.see(tk.END)
        self.consola.configure(state="disabled")

    def limpiar_consola(self):
        self.consola.configure(state="normal")
        self.consola.delete("1.0", tk.END)
        self.consola.configure(state="disabled")

    # ------------------------------------------------------------------
    # Barra de estado
    # ------------------------------------------------------------------
    def _crear_barra_estado(self):
        self.estado = tk.Label(self.root, text="Listo.", bg=COLORES["barra"], fg=COLORES["texto_tenue"],
                                anchor="w", padx=10, font=("Segoe UI", 9))
        self.indicador_cursor = tk.Label(self.root, text="Ln 1, Col 1", bg=COLORES["barra"],
                                          fg=COLORES["texto_tenue"], anchor="e", padx=10, font=("Segoe UI", 9))
        self.indicador_cursor.pack(side=tk.BOTTOM, fill=tk.X)
        self.estado.pack(side=tk.BOTTOM, fill=tk.X)

    def _fijar_estado(self, texto):
        self.estado.configure(text=texto)

    def _actualizar_indicador_cursor(self, evento=None):
        try:
            fila, col = self.texto.index(tk.INSERT).split(".")
            self.indicador_cursor.configure(text=f"Ln {fila}, Col {int(col) + 1}")
        except Exception:
            pass

    # ------------------------------------------------------------------
    # Resaltado de sintaxis (propio de SiPi Electron)
    # ------------------------------------------------------------------
    def _al_escribir(self, evento=None):
        self._actualizar_numeros_linea()
        self._resaltar_sintaxis()

    def _al_presionar_enter(self, evento=None):
        """Mantiene la sangria de la linea anterior en la nueva linea, y
        agrega una sangria extra si la linea abre un bloque (termina con
        una palabra clave de apertura como 'si'/'mientras'/'funcion')."""
        if not self.autoindentar:
            return None
        n = self._linea_actual_num()
        linea = self.texto.get(f"{n}.0", f"{n}.end")
        sangria = re.match(r"[ \t]*", linea).group(0)
        primera_palabra = linea.strip().split(" ")[0] if linea.strip() else ""
        if primera_palabra in ("si", "mientras", "repetir", "funcion") and not linea.strip().startswith("fin"):
            sangria += "    "
        self.texto.insert(tk.INSERT, "\n" + sangria)
        self._al_escribir()
        return "break"

    def _al_modificar(self, evento=None):
        self.modificado = True
        self.texto.edit_modified(False)
        self._actualizar_titulo()

    def _actualizar_numeros_linea(self):
        contenido = self.texto.get("1.0", "end-1c")
        n_lineas = contenido.count("\n") + 1
        self.numeros.configure(state="normal")
        self.numeros.delete("1.0", tk.END)
        self.numeros.insert("1.0", "\n".join(str(i) for i in range(1, n_lineas + 1)))
        self.numeros.configure(state="disabled")

    def _resaltar_sintaxis(self):
        for etiqueta in ("clave", "cadena", "comentario", "numero", "hardware"):
            self.texto.tag_remove(etiqueta, "1.0", tk.END)

        contenido = self.texto.get("1.0", "end-1c")

        for m in re.finditer(r'"[^"\n]*"', contenido):
            self._marcar(m.start(), m.end(), "cadena")
        for m in re.finditer(r'//[^\n]*', contenido):
            self._marcar(m.start(), m.end(), "comentario")
        for m in re.finditer(r'/\*.*?\*/', contenido, re.DOTALL):
            self._marcar(m.start(), m.end(), "comentario")
        for m in re.finditer(r'\b\d+(\.\d+)?\b', contenido):
            self._marcar(m.start(), m.end(), "numero")
        for m in re.finditer(r'[A-Za-zÁÉÍÓÚáéíóúñÑ_][A-Za-zÁÉÍÓÚáéíóúñÑ0-9_]*', contenido):
            palabra = m.group(0)
            if palabra in PALABRAS_CLAVE:
                self._marcar(m.start(), m.end(), "clave")
            elif palabra in FUNCIONES_HARDWARE:
                self._marcar(m.start(), m.end(), "hardware")

    def _marcar(self, inicio, fin, etiqueta):
        self.texto.tag_add(etiqueta, f"1.0+{inicio}c", f"1.0+{fin}c")

    # ------------------------------------------------------------------
    # Archivo
    # ------------------------------------------------------------------
    def _cargar_texto(self, contenido):
        self.texto.delete("1.0", tk.END)
        self.texto.insert("1.0", contenido)
        self.texto.edit_modified(False)
        self.modificado = False
        self._actualizar_numeros_linea()

    def nuevo(self):
        if self._confirmar_descarte():
            self.ruta_archivo = None
            self._cargar_texto("programa \"Nuevo programa\"\n\ndecir \"Hola desde SiPi Electron\"\n")
            self._resaltar_sintaxis()
            self._actualizar_titulo()

    def abrir(self):
        if not self._confirmar_descarte():
            return
        ruta = filedialog.askopenfilename(
            title="Abrir archivo de SiPi Electron",
            filetypes=[("SiPi Electron", "*.spe"), ("Todos los archivos", "*.*")])
        if not ruta:
            return
        try:
            with open(ruta, "r", encoding="utf-8") as f:
                contenido = f.read()
        except OSError as e:
            messagebox.showerror("SiPi Electron", f"No se pudo abrir el archivo:\n{e}")
            return
        self.ruta_archivo = ruta
        self._cargar_texto(contenido)
        self._resaltar_sintaxis()
        self._actualizar_titulo()
        self._registrar_reciente(ruta)
        self._fijar_estado(f"Abierto: {ruta}")

    def guardar(self):
        if not self.ruta_archivo:
            return self.guardar_como()
        self._escribir_archivo(self.ruta_archivo)

    def guardar_como(self):
        ruta = filedialog.asksaveasfilename(
            title="Guardar como", defaultextension=".spe",
            filetypes=[("SiPi Electron", "*.spe"), ("Todos los archivos", "*.*")])
        if not ruta:
            return
        self.ruta_archivo = ruta
        self._escribir_archivo(ruta)

    def _escribir_archivo(self, ruta):
        try:
            with open(ruta, "w", encoding="utf-8") as f:
                f.write(self.texto.get("1.0", "end-1c"))
        except OSError as e:
            messagebox.showerror("SiPi Electron", f"No se pudo guardar:\n{e}")
            return
        self.modificado = False
        self._actualizar_titulo()
        self._registrar_reciente(ruta)
        self._fijar_estado(f"Guardado: {ruta}")

    def _confirmar_descarte(self):
        if not self.modificado:
            return True
        respuesta = messagebox.askyesnocancel("SiPi Electron", "Hay cambios sin guardar. ¿Guardar antes de continuar?")
        if respuesta is None:
            return False
        if respuesta:
            self.guardar()
        return True

    def _actualizar_titulo(self):
        nombre = os.path.basename(self.ruta_archivo) if self.ruta_archivo else "sin guardar"
        marca = "●" if self.modificado else ""
        self.root.title(f"SiPi Electron — {nombre} {marca}".strip())

    # ------------------------------------------------------------------
    # Ejecucion: corre el binario real de SiPi Electron como subproceso
    # ------------------------------------------------------------------
    def _archivo_para_ejecutar(self):
        """Devuelve una ruta .spe con el contenido actual del editor. Si el
        archivo abierto esta guardado y sin cambios, se usa tal cual; si
        no, se guarda a un temporal para no forzar guardar antes de probar."""
        contenido = self.texto.get("1.0", "end-1c")
        if self.ruta_archivo and not self.modificado:
            return self.ruta_archivo
        import tempfile
        tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".spe", delete=False, encoding="utf-8")
        tmp.write(contenido)
        tmp.close()
        return tmp.name

    def _asegurar_binario(self):
        binario = _ruta_binario()
        if os.path.isfile(binario) and os.access(binario, os.X_OK):
            return binario
        self._log("Compilando el motor de SiPi Electron (primera vez)...", "info")
        try:
            resultado = subprocess.run(["make"], cwd=_raiz_proyecto(),
                                        capture_output=True, text=True, timeout=60)
        except (OSError, subprocess.TimeoutExpired) as e:
            self._log(f"No se pudo compilar automaticamente: {e}", "error")
            return None
        if resultado.returncode != 0:
            self._log("Error al compilar SiPi Electron:", "error")
            self._log(resultado.stderr or resultado.stdout, "error")
            return None
        if os.path.isfile(binario):
            return binario
        self._log("No se encontro el binario compilado.", "error")
        return None

    def _fijar_estado_botones_ejecucion(self, corriendo):
        """'Ejecutar' y 'Detener' ahora reflejan el estado real en vez de
        quedarse siempre clickeables y depender de un cartel emergente
        despues del clic (que era lo unico que evitaba, por ejemplo,
        apretar 'Detener' sin que haya nada corriendo). Un boton gris y
        sin cursor de mano de "no disponible ahora" es mas claro de un
        vistazo que un mensaje que aparece recien despues de clickear."""
        estado_ejecutar = "disabled" if corriendo else "normal"
        estado_detener = "normal" if corriendo else "disabled"
        try:
            self.boton_ejecutar.configure(state=estado_ejecutar)
            self.boton_detener.configure(state=estado_detener)
        except Exception:
            pass

    def ejecutar(self):
        if self.proceso is not None:
            messagebox.showinfo("SiPi Electron", "Ya hay un programa corriendo. Deten\u00e9lo primero.")
            return

        binario = self._asegurar_binario()
        if not binario:
            return

        archivo = self._archivo_para_ejecutar()
        self.pin_estado.clear()
        self._dibujar_placa()
        self.texto.tag_remove("linea_error", "1.0", tk.END)
        self._log(f"--- Ejecutando {os.path.basename(archivo)} ---", "info")
        self.indicador_estado.configure(text="● Ejecutando", fg=COLORES["ejecutar"])
        self._fijar_estado("Ejecutando...")

        try:
            self.proceso = subprocess.Popen(
                [binario, archivo], cwd=_raiz_proyecto(),
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                text=True, bufsize=1)
        except OSError as e:
            self._log(f"No se pudo ejecutar el programa: {e}", "error")
            self.proceso = None
            self.indicador_estado.configure(text="● Detenido", fg=COLORES["texto_tenue"])
            return

        self._fijar_estado_botones_ejecucion(corriendo=True)
        self.hilo_lectura = threading.Thread(target=self._leer_salida_proceso, daemon=True)
        self.hilo_lectura.start()

    def _leer_salida_proceso(self):
        proc = self.proceso
        try:
            for linea in proc.stdout:
                self.cola_salida.put(linea.rstrip("\n"))
        except Exception as e:
            self.cola_salida.put(f"__ERROR__:{e}")
        finally:
            proc.wait()
            self.cola_salida.put("__FIN__")

    def _procesar_cola_salida(self):
        try:
            while True:
                linea = self.cola_salida.get_nowait()
                if linea == "__FIN__":
                    self._al_terminar_proceso()
                elif linea.startswith("__ERROR__:"):
                    self._log(linea[len("__ERROR__:"):], "error")
                else:
                    self._procesar_linea(linea)
        except queue.Empty:
            pass
        self.root.after(40, self._procesar_cola_salida)

    def _al_terminar_proceso(self):
        self._log("--- Programa finalizado ---", "info")
        self.indicador_estado.configure(text="● Detenido", fg=COLORES["texto_tenue"])
        self._fijar_estado("Listo.")
        self.proceso = None
        self._fijar_estado_botones_ejecucion(corriendo=False)

    RE_MODO = re.compile(r"\[hal\] pin (\d+) configurado como (ENTRADA|SALIDA)")
    RE_DIGITAL = re.compile(r"\[hal\] pin (\d+) -> (ALTO|BAJO)")
    RE_PWM = re.compile(r"\[hal\] pin (\d+) PWM -> (\d+)/255")
    RE_LEER_DIGITAL = re.compile(r"\[hal\] leyendo pin (\d+) digital -> (\d+)")
    RE_LEER_ANALOGICO = re.compile(r"\[hal\] leyendo pin (\d+) analogico -> (\d+)")

    RE_LINEA_ERROR = re.compile(r"[Ll]inea (\d+)")

    def _procesar_linea(self, linea):
        if not linea:
            return
        if "Error" in linea or "error" in linea:
            m = self.RE_LINEA_ERROR.search(linea)
            if m:
                self._ir_a_linea_num(int(m.group(1)))
            self._log(linea, "error")
            return
        if linea.startswith("[hal]"):
            self._log(linea, "hal")
            m = self.RE_MODO.match(linea)
            if m:
                pin, modo = int(m.group(1)), (1 if m.group(2) == "SALIDA" else 0)
                self._actualizar_pin(pin, modo=modo, pwm=None)
                return
            m = self.RE_PWM.match(linea)
            if m:
                pin, valor = int(m.group(1)), int(m.group(2))
                self._actualizar_pin(pin, pwm=valor)
                return
            m = self.RE_DIGITAL.match(linea)
            if m:
                pin, valor = int(m.group(1)), (1 if m.group(2) == "ALTO" else 0)
                self._actualizar_pin(pin, digital=valor, pwm=None)
                return
            m = self.RE_LEER_DIGITAL.match(linea) or self.RE_LEER_ANALOGICO.match(linea)
            if m:
                self._destello_lectura(int(m.group(1)))
                return
            return
        self._log(linea, "salida")

    def detener(self):
        if self.proceso is None:
            return
        try:
            self.proceso.terminate()
        except Exception:
            pass
        self._log("--- Programa detenido por el usuario ---", "info")

    # ------------------------------------------------------------------
    # Ejemplos y atajos
    # ------------------------------------------------------------------
    def abrir_ejemplos(self):
        carpeta = os.path.join(_raiz_proyecto(), "examples")
        if not os.path.isdir(carpeta):
            messagebox.showinfo("SiPi Electron", "No se encontro la carpeta de ejemplos.")
            return
        archivos = sorted(f for f in os.listdir(carpeta) if f.endswith(".spe"))
        if not archivos:
            messagebox.showinfo("SiPi Electron", "No hay ejemplos disponibles todavia.")
            return

        ventana = tk.Toplevel(self.root)
        ventana.title("Galer\u00eda de ejemplos")
        ventana.configure(bg=COLORES["fondo"])
        ventana.geometry("360x320")
        tk.Label(ventana, text="Elegi un ejemplo para cargarlo en el editor:",
                 bg=COLORES["fondo"], fg=COLORES["texto"], font=("Segoe UI", 10),
                 pady=10).pack()
        lista = tk.Listbox(ventana, bg=COLORES["fondo_alt"], fg=COLORES["texto"], bd=0,
                            font=("Consolas", 11), selectbackground=COLORES["acento"],
                            highlightthickness=0)
        lista.pack(fill=tk.BOTH, expand=True, padx=12, pady=(0, 12))
        for a in archivos:
            lista.insert(tk.END, a)

        def _cargar_seleccion(evento=None):
            seleccion = lista.curselection()
            if not seleccion:
                return
            if not self._confirmar_descarte():
                return
            ruta = os.path.join(carpeta, archivos[seleccion[0]])
            with open(ruta, "r", encoding="utf-8") as f:
                self._cargar_texto(f.read())
            self.ruta_archivo = ruta
            self._resaltar_sintaxis()
            self._actualizar_titulo()
            ventana.destroy()

        lista.bind("<Double-Button-1>", _cargar_seleccion)

    def _atajos_teclado(self):
        self.root.bind_all("<Control-n>", lambda e: self.nuevo())
        self.root.bind_all("<Control-o>", lambda e: self.abrir())
        self.root.bind_all("<Control-s>", lambda e: self.guardar())
        self.root.bind_all("<Control-S>", lambda e: self.guardar_como())
        self.root.bind_all("<Control-Shift-S>", lambda e: self.guardar_como())
        self.root.bind_all("<F5>", lambda e: self.ejecutar())
        self.root.bind_all("<Control-Return>", lambda e: self.ejecutar())
        self.root.bind_all("<Control-f>", lambda e: self.abrir_buscar())
        self.root.bind_all("<Control-h>", lambda e: self.abrir_buscar_reemplazar())
        self.root.bind_all("<Control-g>", lambda e: self.ir_a_linea())
        self.root.bind_all("<Control-plus>", lambda e: self.cambiar_zoom(1))
        self.root.bind_all("<Control-equal>", lambda e: self.cambiar_zoom(1))
        self.root.bind_all("<Control-minus>", lambda e: self.cambiar_zoom(-1))
        self.root.bind_all("<Control-0>", lambda e: self.cambiar_zoom(0))
        self.root.bind_all("<Shift-F5>", lambda e: self.detener())
        self.root.bind_all("<Control-a>", self.seleccionar_todo)
        self.root.bind_all("<Control-slash>", self.comentar_descomentar)
        self.root.bind_all("<Control-d>", self.duplicar_linea)
        self.root.bind_all("<Control-Shift-K>", self.eliminar_linea)
        self.root.bind_all("<Alt-Up>", lambda e: self.mover_linea(-1))
        self.root.bind_all("<Alt-Down>", lambda e: self.mover_linea(1))
        self.root.bind_all("<Control-Shift-T>", lambda e: self.alternar_tema())

    def _al_cerrar(self):
        if self._confirmar_descarte():
            self.detener()
            self._guardar_config_actual()
            self.root.destroy()

    def _guardar_config_actual(self):
        self.config["geometria"] = self.root.geometry()
        self.config["tamano_fuente"] = self.tamano_fuente
        self.config["placa"] = self.nombre_placa
        self.config["recientes"] = self.recientes
        self.config["tema"] = self.tema
        self.config["autoindentar"] = self.autoindentar
        self.config["ajuste_linea"] = self.ajuste_linea
        _guardar_config(self.config)

    # ------------------------------------------------------------------
    # Archivos recientes
    # ------------------------------------------------------------------
    def _registrar_reciente(self, ruta):
        ruta = os.path.abspath(ruta)
        if ruta in self.recientes:
            self.recientes.remove(ruta)
        self.recientes.insert(0, ruta)
        self.recientes = self.recientes[:8]
        self._reconstruir_menu_recientes()

    def _reconstruir_menu_recientes(self):
        self.menu_recientes.delete(0, tk.END)
        if not self.recientes:
            self.menu_recientes.add_command(label="(vacio)", state="disabled")
            return
        for ruta in self.recientes:
            self.menu_recientes.add_command(
                label=os.path.basename(ruta) + f"  —  {ruta}",
                command=lambda r=ruta: self._abrir_ruta(r))

    def _abrir_ruta(self, ruta):
        if not self._confirmar_descarte():
            return
        try:
            with open(ruta, "r", encoding="utf-8") as f:
                contenido = f.read()
        except OSError as e:
            messagebox.showerror("SiPi Electron", f"No se pudo abrir el archivo:\n{e}")
            return
        self.ruta_archivo = ruta
        self._cargar_texto(contenido)
        self._resaltar_sintaxis()
        self._actualizar_titulo()
        self._registrar_reciente(ruta)
        self._fijar_estado(f"Abierto: {ruta}")

    # ------------------------------------------------------------------
    # Buscar / reemplazar / ir a linea
    # ------------------------------------------------------------------
    def abrir_buscar(self):
        self._dialogo_buscar(con_reemplazo=False)

    def abrir_buscar_reemplazar(self):
        self._dialogo_buscar(con_reemplazo=True)

    def _dialogo_buscar(self, con_reemplazo):
        ventana = tk.Toplevel(self.root)
        ventana.title("Buscar y reemplazar" if con_reemplazo else "Buscar")
        ventana.configure(bg=COLORES["fondo"])
        ventana.resizable(False, False)

        tk.Label(ventana, text="Buscar:", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(
            row=0, column=0, sticky="w", padx=8, pady=(10, 4))
        entrada_buscar = tk.Entry(ventana, width=32, bg=COLORES["fondo_alt"], fg=COLORES["texto"],
                                   insertbackground=COLORES["texto"])
        entrada_buscar.grid(row=0, column=1, columnspan=2, padx=8, pady=(10, 4))
        entrada_buscar.focus_set()

        entrada_reemplazo = None
        if con_reemplazo:
            tk.Label(ventana, text="Reemplazar por:", bg=COLORES["fondo"], fg=COLORES["texto"]).grid(
                row=1, column=0, sticky="w", padx=8, pady=4)
            entrada_reemplazo = tk.Entry(ventana, width=32, bg=COLORES["fondo_alt"], fg=COLORES["texto"],
                                          insertbackground=COLORES["texto"])
            entrada_reemplazo.grid(row=1, column=1, columnspan=2, padx=8, pady=4)

        etiqueta_resultado = tk.Label(ventana, text="", bg=COLORES["fondo"], fg=COLORES["texto_tenue"],
                                       font=("Segoe UI", 9))
        etiqueta_resultado.grid(row=2, column=0, columnspan=3, sticky="w", padx=8)

        self.texto.tag_configure("coincidencia", background=COLORES["acento"], foreground="black")

        def _limpiar_marcas():
            self.texto.tag_remove("coincidencia", "1.0", tk.END)

        def _buscar_siguiente(evento=None):
            _limpiar_marcas()
            patron = entrada_buscar.get()
            if not patron:
                return
            inicio = self.texto.index(tk.INSERT)
            pos = self.texto.search(patron, inicio, stopindex=tk.END, nocase=True)
            dio_vuelta = False
            if not pos:
                pos = self.texto.search(patron, "1.0", stopindex=tk.END, nocase=True)
                dio_vuelta = True
            if not pos:
                etiqueta_resultado.configure(text="No se encontro.")
                return
            fin = f"{pos}+{len(patron)}c"
            self.texto.tag_add("coincidencia", pos, fin)
            self.texto.mark_set(tk.INSERT, fin)
            self.texto.see(pos)
            etiqueta_resultado.configure(
                text="Encontrado (volvio al principio)." if dio_vuelta else "Encontrado.")

        def _reemplazar_uno():
            if self.texto.tag_ranges("coincidencia"):
                inicio_r = self.texto.index("coincidencia.first")
                fin_r = self.texto.index("coincidencia.last")
                self.texto.delete(inicio_r, fin_r)
                self.texto.insert(inicio_r, entrada_reemplazo.get())
            _buscar_siguiente()

        def _reemplazar_todos():
            patron = entrada_buscar.get()
            reemplazo = entrada_reemplazo.get()
            if not patron:
                return
            contenido = self.texto.get("1.0", "end-1c")
            n = contenido.count(patron)
            if n == 0:
                etiqueta_resultado.configure(text="No se encontro.")
                return
            nuevo = contenido.replace(patron, reemplazo)
            self._cargar_texto(nuevo)
            self._resaltar_sintaxis()
            etiqueta_resultado.configure(text=f"Se reemplazaron {n} coincidencia(s).")

        fila_botones = 3
        marco_botones = tk.Frame(ventana, bg=COLORES["fondo"])
        marco_botones.grid(row=fila_botones, column=0, columnspan=3, pady=(6, 10), padx=8, sticky="w")
        self._boton(marco_botones, "Buscar siguiente", _buscar_siguiente, COLORES["azul"], COLORES["azul_hover"], 150)
        if con_reemplazo:
            self._boton(marco_botones, "Reemplazar", _reemplazar_uno, COLORES["borde"], "#45475a", 110)
            self._boton(marco_botones, "Reemplazar todos", _reemplazar_todos, COLORES["borde"], "#45475a", 150)

        entrada_buscar.bind("<Return>", _buscar_siguiente)
        ventana.bind("<Escape>", lambda e: (_limpiar_marcas(), ventana.destroy()))
        ventana.protocol("WM_DELETE_WINDOW", lambda: (_limpiar_marcas(), ventana.destroy()))

    def ir_a_linea(self):
        n_lineas = int(self.texto.index("end-1c").split(".")[0])
        numero = simpledialog.askinteger(
            "Ir a linea", f"Numero de linea (1-{n_lineas}):",
            minvalue=1, maxvalue=n_lineas, parent=self.root)
        if numero is None:
            return
        self._ir_a_linea_num(numero)

    def _ir_a_linea_num(self, numero):
        self.texto.mark_set(tk.INSERT, f"{numero}.0")
        self.texto.see(f"{numero}.0")
        self.texto.tag_remove("linea_error", "1.0", tk.END)
        self.texto.tag_add("linea_error", f"{numero}.0", f"{numero}.end")
        self.texto.focus_set()

    # ------------------------------------------------------------------
    # Snippets
    # ------------------------------------------------------------------
    def insertar_snippet(self, nombre):
        codigo = SNIPPETS.get(nombre)
        if not codigo:
            return
        self.texto.insert(tk.INSERT, ("\n" if self.texto.index(tk.INSERT) != "1.0" else "") + codigo)
        self._al_escribir()
        self._fijar_estado(f"Insertado: {nombre}")

    # ------------------------------------------------------------------
    # Zoom de fuente
    # ------------------------------------------------------------------
    def cambiar_zoom(self, delta):
        if delta == 0:
            self.tamano_fuente = 12
        else:
            self.tamano_fuente = max(8, min(28, self.tamano_fuente + delta))
        self._aplicar_tamano_fuente()

    def _aplicar_tamano_fuente(self):
        nueva = ("Consolas", self.tamano_fuente)
        self.texto.configure(font=nueva)
        self.numeros.configure(font=nueva)
        self._fijar_estado(f"Zoom: {self.tamano_fuente}pt")

    # ------------------------------------------------------------------
    # Placa: cambiar de preset en caliente
    # ------------------------------------------------------------------
    def elegir_placa(self, nombre_placa):
        self.nombre_placa = nombre_placa
        self.pin_estado.clear()
        self._dibujar_placa()
        self._fijar_estado(f"Placa: {nombre_placa}")

    def recompilar_motor(self):
        try:
            binario = _ruta_binario()
            if os.path.isfile(binario):
                os.remove(binario)
        except OSError:
            pass
        self._log("Recompilando el motor de SiPi Electron...", "info")
        binario = self._asegurar_binario()
        if binario:
            self._log("Motor recompilado correctamente.", "info")

    # ------------------------------------------------------------------
    # Edicion avanzada: comentar, duplicar, mover y borrar lineas
    # ------------------------------------------------------------------
    def seleccionar_todo(self, evento=None):
        self.texto.tag_add("sel", "1.0", tk.END)
        return "break"

    def _linea_actual_num(self):
        return int(self.texto.index(tk.INSERT).split(".")[0])

    def comentar_descomentar(self, evento=None):
        try:
            inicio = int(self.texto.index("sel.first").split(".")[0])
            fin = int(self.texto.index("sel.last").split(".")[0])
        except tk.TclError:
            inicio = fin = self._linea_actual_num()

        lineas_contenido = [self.texto.get(f"{n}.0", f"{n}.end") for n in range(inicio, fin + 1)]
        no_vacias = [l for l in lineas_contenido if l.strip()]
        todas_comentadas = bool(no_vacias) and all(l.lstrip().startswith("//") for l in no_vacias)

        for n in range(inicio, fin + 1):
            linea = self.texto.get(f"{n}.0", f"{n}.end")
            if not linea.strip():
                continue
            if todas_comentadas:
                pos = linea.find("//")
                if pos != -1:
                    self.texto.delete(f"{n}.{pos}", f"{n}.{pos + 2}")
                    if self.texto.get(f"{n}.{pos}", f"{n}.{pos + 1}") == " ":
                        self.texto.delete(f"{n}.{pos}", f"{n}.{pos + 1}")
            else:
                self.texto.insert(f"{n}.0", "// ")
        self._al_escribir()
        self._fijar_estado("Comentario alternado.")
        return "break"

    def duplicar_linea(self, evento=None):
        n = self._linea_actual_num()
        contenido = self.texto.get(f"{n}.0", f"{n}.end")
        self.texto.insert(f"{n}.end", "\n" + contenido)
        self._al_escribir()
        return "break"

    def eliminar_linea(self, evento=None):
        n = self._linea_actual_num()
        self.texto.delete(f"{n}.0", f"{n + 1}.0")
        self._al_escribir()
        return "break"

    def mover_linea(self, direccion):
        n = self._linea_actual_num()
        n_lineas = int(self.texto.index("end-1c").split(".")[0])
        destino = n + direccion
        if destino < 1 or destino > n_lineas:
            return "break"
        col = int(self.texto.index(tk.INSERT).split(".")[1])
        linea_actual = self.texto.get(f"{n}.0", f"{n}.end")
        linea_destino = self.texto.get(f"{destino}.0", f"{destino}.end")
        self.texto.delete(f"{n}.0", f"{n}.end")
        self.texto.insert(f"{n}.0", linea_destino)
        self.texto.delete(f"{destino}.0", f"{destino}.end")
        self.texto.insert(f"{destino}.0", linea_actual)
        self.texto.mark_set(tk.INSERT, f"{destino}.{col}")
        self.texto.see(f"{destino}.0")
        self._al_escribir()
        return "break"

    # ------------------------------------------------------------------
    # Exportar la consola a un archivo de texto (util para reportar bugs
    # del motor en C o guardar el resultado de una corrida larga)
    # ------------------------------------------------------------------
    def exportar_consola(self):
        contenido = self.consola.get("1.0", "end-1c")
        if not contenido.strip():
            messagebox.showinfo("SiPi Electron", "La consola esta vacia, no hay nada que exportar.")
            return
        ruta = filedialog.asksaveasfilename(
            title="Exportar consola", defaultextension=".txt",
            filetypes=[("Texto", "*.txt"), ("Todos los archivos", "*.*")])
        if not ruta:
            return
        try:
            with open(ruta, "w", encoding="utf-8") as f:
                f.write(contenido)
            self._fijar_estado(f"Consola exportada a: {ruta}")
        except OSError as e:
            messagebox.showerror("SiPi Electron", f"No se pudo exportar la consola:\n{e}")

    # ------------------------------------------------------------------
    # Tema claro/oscuro, ajuste de linea y auto-indentado
    # ------------------------------------------------------------------
    def alternar_tema(self):
        self.tema = "claro" if self.tema == "oscuro" else "oscuro"
        self._aplicar_tema()

    def _aplicar_tema(self):
        paleta = COLORES_CLARO if self.tema == "claro" else COLORES
        self.root.configure(bg=paleta["fondo"])
        for w in (self.barra, self.barra2):
            try:
                w.configure(fg_color=paleta["barra"]) if _CTK_DISPONIBLE else w.configure(bg=paleta["barra"])
            except Exception:
                pass
        self.texto.configure(bg=paleta["fondo"], fg=paleta["texto"], insertbackground=paleta["acento"],
                              selectbackground=paleta["borde"])
        self.numeros.configure(bg=paleta["fondo_alt"], fg=paleta["texto_tenue"])
        self.consola.configure(bg=paleta["fondo_alt"] if self.tema == "claro" else "#11111b", fg=paleta["texto"])
        self.canvas_placa.configure(bg=paleta["panel"])
        for etiqueta, color in [("clave", paleta["clave"]), ("cadena", paleta["cadena"]),
                                 ("comentario", paleta["comentario"]), ("numero", paleta["numero"]),
                                 ("hardware", paleta["builtin"])]:
            self.texto.tag_configure(etiqueta, foreground=color)
        self.estado.configure(bg=paleta["barra"], fg=paleta["texto_tenue"])
        self.indicador_cursor.configure(bg=paleta["barra"], fg=paleta["texto_tenue"])
        self._dibujar_placa()
        self._fijar_estado(f"Tema: {self.tema}")

    def _alternar_ajuste_linea(self):
        self.ajuste_linea = self.var_ajuste.get()
        self._aplicar_ajuste_linea()

    def _aplicar_ajuste_linea(self):
        self.texto.configure(wrap="word" if self.ajuste_linea else "none")

    def _alternar_autoindentar(self):
        self.autoindentar = self.var_autoindentar.get()


def main():
    if shutil.which("cc") is None and not os.path.isfile(_ruta_binario()):
        print("Aviso: no se encontro un compilador de C ('cc'). SiPi Electron podria "
              "no poder compilarse la primera vez que apretes 'Ejecutar'.", file=sys.stderr)

    if _CTK_DISPONIBLE:
        root = ctk.CTk()
    else:
        root = tk.Tk()
    EditorSipiElectron(root)
    root.mainloop()


if __name__ == "__main__":
    main()
