en debian

XWPE 2026: arqueología de software de un editor de programación de 1993

Por qué hago esto

En los 90, cuando debería haber estado terminando la carrera, jugaba
al Unix Conquer en AIX, discutía en Usenet, merodeaba por IRC y
aprendía cómo funcionaban de verdad los sistemas. No conseguí el título, pero conseguí algo que la carrera no podía enseñar: el instinto de cómo debe sentirse el software UNIX.

Tres décadas después, ese instinto es lo que mueve este rescate. Soy
colaborador de Debian camino de convertirme en Debian Developer: todavía no lo soy, aún necesito patrocinadores para mis subidas. xwpe es uno de mis proyectos fuente adoptados y mi camino de entrada en la comunidad Debian.

Rescatar un editor de 1993 que nadie más quería tocar es justo el tipo de cosa que pasa cuando alguien que creció en la cultura de terminal de los 90 por fin tiene las herramientas y el tiempo para devolver algo.

El nombre en clave

La serie 1.6.x es lo que yo llamaba en mi cabeza «xwpe 2026», y es el ciclo de publicación que trajo al presente 33 años de código dormido. No es una reescritura; es una restauración. Archivamos la documentación dispersa que la web primitiva estaba perdiendo, escribimos el manual que el proyecto siempre necesitó
pero nunca tuvo (en texinfo), y arreglamos cada capa de la pila de renderizado e integración con el compilador para que un usuario de 2026 pueda abrir incluso un fichero .pas (Pascal) o .f90 (Fortran), pulsar F9 y navegar por los errores exactamente como Fred Kruse pretendió en 1993, pero esta vez con UTF-8 en un terminal moderno.

El fósil

xwpe es un entorno de programación al estilo Borland para UNIX, escrito por Fred Kruse en 1993. Dennis Payne lo mantuvo de 2000 a 2006, y después silencio. Toda la gente que contribuyó a él probablemente se pasó a otros IDEs… y Debian lo siguió distribuyendo durante 20 años como un paquete huérfano de hecho: compilaba, así que nadie miró más de cerca. Los usuarios curiosos abrían informes de fallo (algunos estuvieron abiertos 22 años)

En mayo de 2026, GCC 15 rompió la compilación (#1098180). Andreas Tille pidió ayuda en el canal de IRC #debian-tiny-tasks. Ahí entré yo… recordaba xwpe de mis años de universidad y esta podía ser mi primera intervención para rescatar un paquete, y no solo para empaquetar nuevos.

Arqueología digital

Restaurar xwpe se parece a la arqueología de software, y empecé a querer ese título cuando hice algo parecido con conquer, el juego clásico de Unix de los 80 de Ed Barlow y Adam Bryant. Después de que alguien dejara un comentario en esa dirección en Hacker News, y como me gustaba que se me considerara así, lo adopté sin más.

Con la arqueología de software apartas capas de polvo y encuentras código que fue sólido para su época: un analizador de errores del compilador basado en patrones,
un sistema de renderizado con doble búfer manual, guardado y restauración de ventanas emergentes superpuestas. Todo diseñado en 1993, cuando ncurses era joven y los terminales eran lentos.

Pero se habían instalado 30 años de podredumbre acumulada:

  • La migración a SCREENCELL (v1.6.0) cambió las celdas de pantalla de 2 bytes a 8 bytes por el UTF-8. El sistema de guardado y restauración de ventanas emergentes nunca se adaptó del todo: datos del montón sin inicializar se propagaban por los ciclos de guardado y restauración, produciendo caracteres CJK y artefactos en pantalla al cerrar los menús. valgrind mostró 15,857 errores de valor sin inicializar.
  • El analizador de errores del compilador esperaba el entrecomillado con acentos graves del GCC de los años 90 `ret'), pero el GCC moderno usa comillas simples ('ret'). El patrón gnu_intstr no se había actualizado en décadas. La navegación por errores (Alt-T/Alt-V) estaba rota en silencio. Lo actualizamos al formato estándar file:line:column: que GCC usa desde la 4.8 (2013).
  • print_to_end_of_buffer se paraba en la primera línea en blanco con un break. Eso significaba que cualquier compilador que emite líneas en blanco entre errores (gfortran, clang con diagnósticos de caret) solo mostraba el primer error en la ventana Messages. Un fallo de una sola línea con más de 20 años de edad.
  • Nombres de compilador por defecto: f77 para Fortran, pc para Pascal. Nadie los había actualizado desde los años 90. Los actualizamos a gfortran (con .f90/.f95/.f03/.f08), fpc (con .pas/.pp y un patrón dedicado ${FILE}(${LINE},${COLUMN})*), y añadimos javac como quinto compilador por defecto.
  • Flags -c -o escritos a fuego: e_comp() pasaba los -c y -o propios de gcc a todos los compiladores. Free Pascal y javac se atragantaban con -c. Hicimos que los flags dependieran del estilo de compilador: los compiladores GNU los reciben, los que no son GNU no.
  • Código de salida del compilador ignorado: ret = 0 estaba escrito a fuego después de
    wait() desde la época de Payne. xwpe nunca sabía cuándo fallaba la compilación. Arreglado con WIFEXITED/WEXITSTATUS.

Probamos cada compilador de forma interactiva: escribir un programa con
errores deliberados en dos funciones separadas, pulsar F9, comprobar que los errores
aparecen en Messages, pulsar Alt-T/Alt-V para navegar entre ellos, verificar que el cursor salta a la línea y columna correctas. gcc, g++, gfortran, fpc y javac pasaron todos. El motor de coincidencia de patrones de Kruse de 1993 (${FILE}, ${LINE}, ${COLUMN}, comodines) los manejó todos y solo tuvimos que actualizar los patrones por defecto y enseñar a e_comp() que no todos los compiladores son gcc.

Cada arreglo revela la capa siguiente. Arreglas el búfer SCREENCELL y por fin puedes ver los errores del compilador. Arreglas la visualización de errores y descubres que la navegación por errores está rota. Arreglas el patrón y encuentras que el búfer de salida se estaba truncando. Arreglas el truncamiento y descubres que los valores por defecto del compilador son de 1993.

Despertando al depurador

La capa más profunda era el depurador. xwpe habla con gdb a través de tuberías: hace fork de un hijo, lanza gdb y se comunica escribiendo comandos y leyendo respuestas. Esta es la función que convirtió a xwpe en un IDE, no solo en un editor. Y llevaba años muerta.

Tres fallos, apilados como estratos geológicos:

Estrato 1: canibalismo SIGCHLD. El manejador de señal para la muerte de procesos
hijos usaba wait(), que recolecta cualquier hijo. Cuando xwpe
llamaba a popen("tty") para saber el nombre de su terminal, el pequeño subproceso tty
terminaba, disparaba SIGCHLD y wait() se lo comía.
Después pclose() llamaba a waitpid() por un proceso que ya
no estaba. Bloqueo. El arreglo: waitpid(-1, &statloc, WNOHANG): sin bloqueo, recolecta solo lo que esté listo.

Estrato 2: compilación fantasma. Cuando el fichero .o ya
estaba al día, xwpe se saltaba el compilador pero aun así llamaba a la función
que espera la salida del compilador. wait() sin ningún proceso hijo falla
al instante, y xwpe concluía que la compilación había fallado. El
depurador nunca llegaba a arrancar.

Estrato 3: enlazado fantasma. El mismo fallo, una función más arriba. El
ejecutable estaba al día, se saltaba el enlazado, pero el código de espera del
enlazador seguía ejecutándose y «fallaba».

Cada estrato tapaba el de abajo. No podías ver el fallo de SIGCHLD hasta que se arreglaban las compilaciones fantasma. No podías ver esas hasta que se arreglaba el renderizado SCREENCELL. No podías arreglar el renderizado hasta que el UTF-8 funcionaba.

Después de los tres arreglos: Ctrl-G B pone un punto de interrupción. Ctrl-G R arranca gdb. F8 recorre el código paso a paso. Las variables se actualizan en la ventana Watch. Ahí fue cuando me cayó encima la sensación de dicha, al ver los puntos de interrupción y los colores de la línea moviéndose sobre cada línea.

La pila de llamadas muestra la cadena de funciones. Funciona exactamente como Borland Turbo Debugger, que es justo lo que Fred Kruse estaba construyendo en 1993.

El problema de la pantalla de usuario

Pero ¿a dónde va la salida de printf? En el IDE DOS de Borland, el programa tenía su propia página de vídeo: cambiar era un vuelco de registro de hardware, instantáneo. En los terminales UNIX, Kruse usaba smcup/rmcup (búfer de pantalla alternativa) para cambiar entre el editor y la salida del programa.
Cada paso de F8: rmcup, gdb ejecuta una línea, smcup de vuelta. En un VT220 de 1993 por serie, esto era invisible. En un emulador de terminal de 2026 por SSH, es un parpadeo que distrae.

La solución de XWPE 1.5 era desactivar por completo la pantalla de usuario bajo ncurses con una guarda #ifndef NCURSES rota que hacía que Ctrl-G P siempre devolviera un error. Problema escondido, no resuelto.

Nuestro enfoque: eliminar por completo el cambio de pantalla. Usar un
pseudoterminal (pty) para capturar la salida del programa en memoria. El programa escribe a un esclavo pty (que parece un terminal real: isatty() devuelve true, stdout tiene búfer por líneas). xwpe lee del maestro pty a un búfer. Ctrl-G P muestra el búfer. Sin smcup/rmcup durante los pasos, cero parpadeo.

Pero hay una trampa con la que tropieza todo desarrollador de IDEs: glibc almacena en búfer
stdout incluso cuando está conectado a un pty, porque el pty lo abre gdb (no se hereda como fd 1). La salida se queda en un búfer de espacio de usuario dentro del proceso depurado y nunca llega al pty hasta que el programa termina o llama a fflush(). La solución, compartida por
gdbgui, Eclipse CDT y VS Code cpptools:
después de cada paso, enviar call (void)fflush(0) a gdb. Esto fuerza el vaciado del búfer de stdout del proceso depurado, y la salida llega al maestro pty donde xwpe puede leerla.

El mismo patrón se aplica a la integración con jdb (Java Debugger). El
arranque de la VM de jdb es asíncrono: envías run y el prompt vuelve al instante, pero el punto de interrupción salta segundos después, cuando la JVM termina de cargar. Una lectura bloqueante ingenua se queda en un bucle al 40% de CPU esperando una salida que aún no ha llegado.

La solución, tomada de JDEE (el IDE de Java para Emacs que resolvió esto en los 2000): usar poll() con un tiempo de espera. Cero CPU mientras esperas, respuesta inmediata cuando llegan datos, error limpio si la JVM no arranca. JDEE usa un patrón acumulador
para almacenar la salida parcial y buscar patrones de prompt: hacemos lo
mismo en C con strstr() sobre un búfer que crece.

La lección: el código de 1993 no necesita patrones de integración de 1993. poll(), ptys, fflush(0): son las mismas herramientas que usan Eclipse CDT y VS Code. La única diferencia es que las escribimos en 50 líneas de C en vez de 5000 líneas de TypeScript.

Cómo depuramos al depurador

Restaurar el depurador de un IDE significa depurar al propio depurador. Usamos todas las técnicas del manual, subiendo el nivel a medida que los fallos eran más profundos:

Nivel 1: fprintf a fichero. La primera herramienta. Cuando aparecieron los
artefactos de las ventanas emergentes SCREENCELL, escribíamos los valores de las celdas a
/tmp/xwpe-switch-trace.txt antes y después de cada ciclo de guardado y restauración. Esto reveló que los búferes reservados con malloc contenían datos del montón sin inicializar: el arreglo fue calloc.

Nivel 2: valgrind. Cuando fprintf no bastaba, valgrind mostró
15,857 errores de valor sin inicializar del sistema de búferes schirm.
Todos se remontaban a la migración SCREENCELL, que cambió las celdas de 2 a 8 bytes sin actualizar las rutas de reserva de memoria.

Nivel 3: ficheros de traza de gdb. Para la comunicación del depurador,
registramos cada comando enviado a gdb y cada respuesta recibida:
TX: r > /dev/pts/5, RX: Starting program:, RX: (gdb). Esto reveló que el patrón gnu_intstr estaba congelado en los años 90 (entrecomillado con acento grave), que print_to_end_of_buffer se paraba en las líneas en blanco y que WpeSignalChild robaba procesos hijos a pclose.

Nivel 4: reproducción independiente. Cuando la captura de salida del pty
no funcionaba, escribimos un programa C de 60 líneas que reproducía la configuración exacta de tubería+fork+pty de xwpe. La prueba funcionaba; xwpe no. La diferencia: el tiempo. La función de drenaje leía el maestro pty antes de que gdb hubiera entregado los datos. Mover el drenaje a después de
call (void)fflush(0) lo arregló.

Nivel 5: trazado con compilación condicional. Para la integración con jdb,
añadimos instrumentación #ifdef JDB_TRACE por toda la ruta de comunicación del depurador. Compilar con -DJDB_TRACE, ejecutar, leer /tmp/xwpe-jdb-trace.txt. La traza mostró cada byte recibido de jdb, cada prompt detectado, cada transición de estado. Esto reveló la causa raíz: una fuga de descriptores de fichero de una tubería de 30 años de antigüedad, en la que el padre mantenía abierto el extremo de escritura de la tubería de stdout tras el fork, lo que impedía que read() detectara el EOF. El arreglo: tres
llamadas close() en el padre, tres en el hijo.

Nivel 6: poll() con tiempo de espera. Para el arranque asíncrono de la VM de jdb,
sustituimos el read() en bucle ocupado por poll() y un tiempo de espera de 10
segundos. Cero CPU mientras esperas, respuesta inmediata cuando llegan
datos, error limpio al agotarse el tiempo. Es el patrón de JDEE (Emacs)
de los 2000, implementado en C.

Cada nivel se apoyaba en el anterior. No puedes usar valgrind hasta que
fprintf te dice dónde mirar. No puedes escribir una prueba independiente
hasta que el registro de traza te dice qué es distinto. No puedes usar
poll() hasta que entiendes por qué read() nunca devuelve EOF.

La infraestructura de trazado se queda en el código. Compila con
-DJDB_TRACE y obtienes un registro completo del protocolo. Así es como
los desarrolladores profesionales de IDEs depuran las integraciones de depuradores: y así es como un código de 1993 aprende a hablar con una JVM de 2026, en unos cientos de líneas de C en vez de un millón de líneas de TypeScript.

La esencia

Lo que más me sorprendió es lo bien que aguanta la arquitectura central. Fred Kruse diseñó un sistema de coincidencia de patrones para los mensajes de error del compilador (${FILE}:${LINE}:${COLUMN}:*) lo bastante flexible como para manejar gcc, g++, gfortran, javac y fpc con configuración por lenguaje.
El sistema de renderizado con doble búfer (schirm/altschirm) con guardado y restauración manual es notablemente eficiente: valoramos migrar a paneles de ncurses pero concluimos que el diseño original es en realidad mejor para el caso de uso de xwpe.

Cada capa que levantábamos revelaba la siguiente. Los fallos nunca estaban en la lógica de Kruse: estaban en la integración: manejadores de señales
interactuando con pclose, estados de terminal que derivaban entre ncurses y smcup/rmcup manual, patrones de error congelados en 1993 mientras los compiladores evolucionaban. La arquitectura era sólida; solo necesitaba que alguien se sentara a escuchar lo que el código intentaba decir.

El objetivo no es reescribir xwpe. Es preservar la esencia del
código de Kruse y Payne mientras arreglamos lo que rompieron 30 años de abandono en los archivos. Una restauración, no una reconstrucción, como encontrar una vieja joya de libro en una biblioteca antigua.

En este punto me vino una analogía nueva. Creo que esto ya no es un fósil ni una vieja nave espacial en el desguace… esto sigue siendo viejo, pero algo empieza a brillar, y las luces vuelven al puente… el Halcón Milenario vuelve a volar.

El ángulo de Debian

Debian tenía xwpe en un cajón. Compilaba, así que pasaba la CI.

Andreas ha sido fundamental para facilitar el traspaso del fuente. Como patrocinador de Debian, guio la NMU que mantuvo xwpe en el archivo mientras yo montaba el nuevo fuente en Codeberg. Sin ese puente, el paquete podría haber sido eliminado de unstable antes de que el rescate pudiera empezar.

Asumir el papel de mantenedor del fuente (con la bendición de Dennis Payne) y mantener a la vez el paquete de Debian cierra el círculo. Los fallos se arreglan en el origen, no se tapan con parches, una tarea imposible en mi opinión. El paquete de Debian
se convierte en una envoltura fina alrededor de un fuente vivo.

Cerrando la lista de fallos

Repasamos a fondo la lista de correo de xwpe de 1998–2003: 487 mensajes. Los usuarios informaban de cuelgues del depurador, errores «Can’t open PIPE», fallos «Error loading program». El cuelgue del depurador de John Connor (junio de 2000). El «el proceso desaparece» de Jan Ypma en Debian (mayo de 2000). El «Can’t start the debugger» de Diego en X11 (octubre de 2000). El «viaje a La-La land» de B O’Donnell (julio de 2001). Eso me ayudó a saber qué podía resolver a continuación.

Veinte años después, podemos rastrear cada uno de ellos hasta los fallos que arreglamos:

  • Cuelgues del depurador: WpeSignalChild usaba wait(), que robaba los procesos hijos a pclose(). La llamada popen(«tty») para saber el nombre del terminal se quedaba colgada para siempre. Arreglado con waitpid(-1, WNOHANG).
  • «Error loading program»: e_comp y e_p_make llamaban a
    e_p_exec(-1) cuando no hacía falta recompilar, lo que provocaba un fallo fantasma de wait(). El depurador concluía que la compilación había fallado y se rendía.
  • Pantalla de usuario rota: la guarda #ifndef NCURSES del xwpe antiguo
    desactivaba sin condiciones Ctrl-G P cuando se compilaba con ncurses: que es siempre, en todas las distribuciones Linux desde finales de los 90. Nadie pudo ver la salida del programa durante la depuración durante más de 20 años.
  • Parpadeo de pantalla al avanzar paso a paso: el diseño original de Kruse cambiaba
    entre la pantalla alternativa y la normal (smcup/rmcup) en cada paso de F7/F8. Invisible en un vuelco de página de vídeo de DOS en 1993, pero un
    destello que distrae en los terminales modernos. Lo sustituimos por un
    pseudoterminal que captura la salida en memoria: cero parpadeo, y Ctrl-G P la muestra limpia.

No eran casos límite oscuros. Eran el flujo central de depuración, roto durante toda la era ncurses. Todo usuario que probó el depurador entre 2000 y 2026 se dio contra uno de estos muros.

Depuración de Java: la integración con jdb

Añadir jdb (Java Debugger) como quinto backend de depuración de xwpe destapó una fuga de descriptores de fichero de una tubería de 30 años que afectaba a TODOS los tipos de depurador. El proceso padre mantenía abierto el extremo de escritura de la tubería de stdout después del fork(), lo que impedía que read() detectara el EOF. gdb lo toleraba por accidente: su sesión nunca termina de forma inesperada. jdb, con su frágil tiempo de arranque de la JVM, chocó con ello de inmediato.

El arreglo: tres llamadas close() en el padre, tres en el hijo.
Un fallo que sobrevivió 30 años porque nadie añadió un segundo backend de depurador lo bastante estricto para destaparlo.

Investigamos cómo manejan jdb otros editores: JDEE
(Emacs, ~2003) analiza la salida de texto de jdb con un patrón acumulador. Vim tiene una envoltura básica de jdb.
Neovim usa nvim-dap con el servidor DAP java-debug. Helix, Kakoune, Micro y Nano no tienen depuración de Java en absoluto, hasta donde yo sé.

xwpe podría ser ahora mismo el único editor de terminal con depuración de Java integrada en el binario: sin plugins, sin un servidor
DAP externo, sin configurar Metals ni java-debug. Abre un fichero .java, pulsa F8, pon un punto de interrupción, recorre el código paso a paso. El depurador elige jdb automáticamente cuando ve una extensión .java. Usa poll() con un tiempo de espera de 10 segundos para esperar a que arranque la JVM (el
patrón acumulador de JDEE, implementado en C). La salida del programa se captura de la tubería de jdb y se muestra con Ctrl-G P.

El método era ajustado: instrumentación de traza (-DJDB_TRACE) a un lado, pruebas interactivas al otro. Ejecutar jdb, observar exactamente qué pasa: «el punto de interrupción no cambia de color», «aparece una ventana emergente pero no debería», «wpe se cierra sin preguntar»: y cada síntoma apunta a una ruta de código concreta. El fichero de traza (/tmp/xwpe-jdb-trace.txt) mostró cada byte intercambiado con jdb. Entre lo que veía en pantalla y el registro de traza, cada fallo se estrechó a una única línea de C.

El Halcón Milenario

xwpe es estos días el Halcón Milenario de los IDEs. Puede que no parezca gran cosa: 3000 líneas de C de 1993, una TUI que dibuja sus propios bordes con caracteres ACS, un motor de coincidencia de patrones para errores del compilador que usa ${FILE}:${LINE}:${COLUMN}:* en vez de expresiones regulares. Pero tiene lo que importa.

Fred Kruse lo construyó en 1993, cuando Linux tenía dos años y gcc
no tenía salida en color. Fue visionario: un ciclo
integrado de compilar-editar-depurar en terminales UNIX, años antes de que existieran Eclipse o VS Code. Que sobreviviera hasta 2006 con fallos como
el manejador SIGCHLD robando procesos hijos y la pantalla de usuario
permanentemente desactivada habla de la solidez del diseño central.

Hace lo que VS Code hace con un millón de líneas de TypeScript y
un runtime Electron. Lo hace en 3000 líneas de C, en 2MB de RAM,
sobre una conexión SSH a un servidor sin pantalla, en una Raspberry Pi, en un contenedor Docker, en una consola serie. En cualquier sitio donde tengas un terminal y un compilador de C, xwpe funciona.

Y habla tu idioma. Los usuarios de Borland tienen los menús guiados por teclas F y el ciclo de compilación F9 que recuerdan. Los usuarios de Emacs tienen Ctrl-P/N/F/Bpara mover el cursor, Ctrl-A/E para inicio y fin de línea: todos los atajos de readline que tienen en la memoria muscular. Kruse integró los dos en el mismo editor en 1993, dos décadas antes de que VS Code ofreciera «Emacs Keymap» como extensión.

La lista de correo se quedó en silencio en 2006. El último informe de usuario fue sobre
un cuelgue del depurador. Ahora sabemos exactamente por qué: wait() en vez de
waitpid(), en un manejador de señales escrito en los años 90, disparado
cada vez que popen("tty") terminaba antes de que pclose() pudiera recolectarlo. Un arreglo de una línea para 20 años de silencio.

El rastro de los fallos: 487 mensajes, 50 issues, una traza

Los fallos más difíciles de encontrar son los que nadie puede reproducir de forma fiable. El cuelgue de Redo: pulsa Ctrl-R después de deshacer un buscar-y-reemplazar y mira cómo xwpe muere con SIGSEGV. Se informó en 2017 como el issue 77 en el fork de GitHub de Alessandro Magnasco. Guus Bonnema intentó investigarlo. Nadie lo arregló. El issue estuvo abierto 9 años.

Lo encontramos siguiendo un rastro a lo largo de 30 años de fuentes dispersas:

  • 487 mensajes de la lista de correo (1998-2003) del
    archivo de identicalsoftware.com de Payne, preservado en nuestro repositorio xwpe-archive
  • 50 issues abiertos del fork de GitHub de Magnasco, triados uno a uno contra nuestro código
  • La reseña checa de Miroslav Janiš (1998), que documentaba el fallo del flag -c que arreglamos 28 años después
  • La rama experimental de Guus Bonnema, que identificó los
    problemas de deshacer en buscar/reemplazar pero no pudo señalar la causa
  • El curso de depurador de LUG-S (2001), con capturas GIF
    transcritas para nuestro manual
  • 9 informes en el BTS de Debian entre 2002-2025

El cuelgue de Redo resultó ser un único campo sin inicializar.

Para entenderlo, piensa en xwpe como si tuviera dos cuadernos. El «cuaderno del editor» es donde la orden Replace anota «acabo de reemplazar 5 caracteres». El «cuaderno de deshacer» es donde Undo lee «¿cuántos caracteres se reemplazaron?» para saber cómo invertir la operación.

El problema: Kruse escribió la respuesta en un cuaderno (el estado global de búsqueda del editor, f->ed->fd.rn) pero la leía de un cuaderno distinto (la copia local de la ventana, b->f->fd.rn). La copia local nunca se rellenaba. Contenía la basura que hubiera en memoria cuando se creó la ventana: en nuestra traza, 189 en vez de 5.

Así que cuando Redo intentaba «rehacer el reemplazo», pensaba que necesitaba guardar 189 caracteres de una línea que solo tenía 5. Escribía más allá del final de su búfer, corrompía la contabilidad interna del asignador de memoria y se estrellaba dentro del propio malloc(): ni siquiera en el código de xwpe.
La traza de la pila mostraba unlink_chunk en lo profundo de libc, lo que es
casi imposible de diagnosticar sin saber qué llevó a la
corrupción.

Lo encontramos añadiendo un registro de traza que imprimía los campos de la entrada de deshacer antes de cada operación. El registro mostraba a=(5,189): cinco caracteres guardados, pero 189 anotados como la longitud del reemplazo. Debería haber sido a=(5,5). El 189 era la pista definitiva.

El arreglo: una línea. b->f->fd.rn = fd->rn; copiar la respuesta del cuaderno correcto al que Undo lee de verdad. Un fallo que existía desde 1993, informado en 2017, diagnosticado en 2026.

Ninguna fuente por sí sola nos habría llevado ahí. La lista de correo nos dijo
qué funciones eran frágiles. Los issues de GitHub nos dijeron qué escenarios provocaban cuelgues. La arqueología del código nos dijo qué campos estaban desincronizados.
Juntos, dibujaron un mapa hacia un arreglo de una línea para un fallo de 33 años.

El ratón: tres décadas, tres backends

Cuando abrimos we_mouse.c, encontramos 1189 líneas de lógica de interfaz de ratón: menús, barras de desplazamiento, cambio de tamaño de ventanas arrastrando las esquinas, selección de texto, arrastrar y soltar entre ventanas del gestor de ficheros. Toda la interacción que esperarías de un IDE de Borland. Todo escrito por Fred Kruse en 1993.

Y al final de we_term.c, el backend de ratón para terminal:

int fk_t_mouse(g)
     int *g;
{
   return(0);
}

Cuatro líneas. Un stub. Un marcador de posición que decía: «todavía no sé cómo funcionará el ratón en los terminales, pero cuando alguien lo averigüe, que lo enchufe aquí».

El enchufe era fk_mouse: un puntero a función. El modo X11 lo ponía a
fk_x_mouse (leyendo eventos ButtonPress de Xlib). El modo terminal lo ponía a este stub. Las 1189 líneas de we_mouse.c nunca supieron ni les importó qué backend estaba activo. Llamaban a fk_mouse(g) a través del puntero y recibían coordenadas. Polimorfismo Unix de libro en C.

En 1998, Sebastiano Suraci, en la Universidad de Catania, enchufó
GPM: el demonio General Purpose Mouse para la consola de Linux. Su we_gpm.c llamaba a Gpm_Open(), instalaba un callback manejador y ponía fk_mouse = WpeGpmMouse. Las 1189 líneas de interfaz de ratón funcionaron al instante. La arquitectura de Kruse aguantó.

Durante 26 años.

Después ncurses cambió. En algún momento entre ncurses 5.x y 6.x, la biblioteca añadió su propia integración interna de GPM. Cuando llamas a mousemask(), ncurses abre ahora su propia conexión GPM, lee los eventos del mismo socket y los entrega como KEY_MOUSE. Dos consumidores peleando por el mismo gpm_fd. Los eventos iban a un manejador o al otro de forma impredecible. Las coordenadas no coincidían: Suraci ponía gpm_zerobased=1
(base 0), ncurses esperaba base 1 y restaba 1 internamente.
Los clics se volvieron erráticos. A veces el menú respondía, a veces seleccionaba texto, a veces no pasaba nada.

El arreglo fue soltar el código de Suraci. No porque estuviera mal: era perfecto para 1998. Pero ncurses 6.x ya hace lo que Suraci hacía a mano. Quitamos el Gpm_Open() directo, dejamos que mousemask() maneje GPM de forma transparente y enchufamos el ratón de ncurses al stub fk_mouse de Kruse. La misma función de cuatro líneas que estaba vacía en 1993 maneja ahora los clics del ratón en xterm, gnome-terminal, kitty, tmux y la consola de Linux con GPM: todo por la misma ruta KEY_MOUSE + getmouse().

Tres backends a lo largo de tres décadas, y we_mouse.c nunca cambió.
Eso es ingeniería.

El arrastre: enseñar a un editor de 1993 a seguir tu mano

El clic funciona. El arrastre es otro mundo.

Y no solo por el ratón, esto me llevó días.

Cuando enchufamos el ratón de ncurses al stub fk_mouse de Kruse, los clics funcionaron al instante: cada menú, barra de desplazamiento y cambio de ventana se encendió a través de las 1189 líneas de we_mouse.c sin un solo cambio. Tres backends, tres décadas, la arquitectura de Kruse aguantó. Ya lo dijimos en el capítulo anterior.

Pero intenta arrastrar la barra de título de una ventana y no pasa nada. La ventana no se mueve. Tu mano recorre el trackpad y el cursor se queda ahí, parpadeando. Porque el modo 1000 de ncurses: el de por defecto: solo informa de pulsar y soltar. ¿Las cien posiciones de ratón que tu mano traza entre medias? Invisibles. El terminal no las envía. El editor nunca las ve.

Entra el modo 1002.

El protocolo que nadie documenta

El modo de ratón 1002 de xterm (\033[?1002h) es el protocolo de seguimiento de arrastre.
Mientras un botón está pulsado, el terminal envía un evento REPORT_MOUSE_POSITION por cada celda que cruza el cursor. Así es como Midnight Commander hace el cambio de tamaño de paneles. Así es como tmux hace el arrastre de paneles. Así es como vim hace la selección visual entre líneas. No el modo 1000 (solo clics), no el modo 1003 (todo movimiento, incluso sin botones: inunda la entrada), sino 1002: movimiento mientras se mantiene el botón. Exactamente lo que necesita el arrastre de ventanas.

Lo activamos en el inicio del terminal. Dos secuencias de escape:
\033[?1002h\033[?1006h. La segunda activa las coordenadas extendidas SGR: sin ella, los terminales de más de 223 columnas codifican basura porque el byte de coordenada X10 se desborda. Ambas se desactivan limpiamente al salir.

Y de repente, fk_t_mouse estaba recibiendo un flujo continuo de
coordenadas. ¡Los bucles de arrastre de Kruse en we_mouse.c: escritos en 1993 para los eventos MotionNotify de X11: empezaron a mover ventanas en ncurses! La barra de título seguía al ratón. Los manejadores de cambio de tamaño de las esquinas funcionaban. Tres décadas de código de arrastre dormido, despertando porque los dos bytes correctos se enviaron al terminal en el inicio.

La trampa de la ventana emergente

Funcionaba a la perfección. Hasta F9.

Pulsa F9 para compilar. Aparece la ventana emergente «Compiling…», el compilador se ejecuta, los resultados van a Messages. Cierra Messages. Ahora intenta arrastrar una ventana.
La barra de título agarra: y no suelta nunca. La ventana sigue a tu ratón para siempre, incluso después de soltar el botón. La interfaz se queda atrapada en un bucle de arrastre infinito.

La causa raíz necesitó tres soluciones provisionales fallidas antes de que dejáramos de parchear y empezáramos a hacer ingeniería.

Esto es lo que pasa. El usuario está clicando tan contento: e_mouse.k dice «botón 1 pulsado» o «botón 1 soltado» y todo funciona. Entonces se abre la ventana emergente de F9. La ventana emergente tiene su propio bucle de eventos. Dentro de ese bucle, ncurses entrega un evento PRESSED (el clic que activó algo dentro de la ventana) y su evento RELEASED correspondiente. El bucle de eventos de la ventana se consume los dos. Sale.

Ahora fk_t_mouse se ejecuta en el bucle de eventos principal. Lee e_mouse.k, que todavía dice «pulsado» por el ÚLTIMO evento que vio el bucle principal: antes de que la ventana emergente robara el soltado. El bucle de arrastre empieza. Pero el botón físico está soltado. No llegará nunca ningún evento RELEASED (la ventana emergente ya se lo comió). El bucle de arrastre espera para siempre.

Estudiamos cómo manejan esto otros frameworks de TUI:

  • Midnight Commander: usa flags de captura de ratón por widget.
    El widget activo reclama el ratón, y cuando lo suelta, el flag se limpia. Pero MC no tiene el gestor de ventanas apilables de xwpe con ventanas emergentes arbitrarias.
  • Turbo Vision (el framework de TUI en C++ de la propia Borland, de 1990):
    usa un bucle de arrastre exclusivo con GetMouseEvent en un bucle
    while(button_held) apretado. El bucle es dueño del ratón hasta que se suelta. Las ventanas emergentes no pueden interrumpir porque el bucle de arrastre no cede al despachador de eventos.
  • El propio ncurses: no proporciona ningún estado de ratón consciente de las ventanas emergentes.
    Informa de eventos. Lo que hagas con ellos es tu problema.

Ni la captura por widget de MC ni el bucle exclusivo de Turbo Vision
encajan en la arquitectura de xwpe. El diseño de Kruse despacha los eventos de ratón
a través de un puntero a función (fk_mouse), y el código de arrastre en
we_mouse.c asume que recibe una secuencia limpia de pulsar-arrastrar-soltar. Las ventanas emergentes rompen esa suposición.

g_mouse_buttons: una máscara de bits para gobernarlas a todas

La solución: separar el estado físico de los botones del despacho de eventos.

int g_mouse_buttons = 0;

Un entero global. El bit 0 es el botón 1. Se actualiza en exactamente dos sitios: e_t_getch (el lector de teclado) pone el bit en PRESSED y lo limpia en RELEASED. fk_t_mouse (el despachador de ratón) lee g_mouse_buttons para decidir si hay un arrastre en marcha, en vez de fiarse de e_mouse.k.

La clave: e_t_getch siempre ve todos los eventos de ratón, incluso dentro de los bucles de eventos de las ventanas emergentes, porque las ventanas emergentes también llaman a e_t_getch. Así que g_mouse_buttons siempre refleja el estado físico real. Cuando el bucle de eventos de la ventana procesa un evento RELEASED, g_mouse_buttons limpia el bit. Cuando la ventana sale y el bucle principal se reanuda, fk_t_mouse lee g_mouse_buttons = 0 y sabe: no hay arrastre. Sin
ratón atascado. Sin bucle infinito.

Pero eso es solo la mitad del arreglo.

e_mouse_flush: drenando a los muertos

Después de que se cierra una ventana emergente, la cola de eventos de ncurses puede seguir conteniendo eventos de ratón viejos: RELEASED fantasma de clics dentro de la ventana, o eventos de movimiento de la mano del usuario mientras la ventana estaba abierta. Si llegan a fk_t_mouse antes de un clic nuevo, confunden a la máquina de estados.

Peor: ncurses a veces desactiva en silencio el modo 1002 durante el procesamiento de eventos de la ventana emergente. El manejador de secuencias de escape se confunde con la salida de la ventana y el seguimiento de arrastre se detiene. El terminal deja de enviar eventos de movimiento. El siguiente intento de arrastre falla en silencio: clicas la barra de título, mantienes el botón, mueves y no pasa nada. Sin error, sin diagnóstico, solo un ratón muerto.

e_mouse_flush() arregla los dos problemas:

  1. Drenar: bucle de getmouse() hasta que devuelva ERR: vaciar la
    cola de eventos viejos.
  2. Reactivar: reenviar \033[?1002h\033[?1006h: forzar el modo 1002 de vuelta, sin importar lo que ncurses hiciera internamente.
  3. Reiniciar: g_mouse_buttons = 0: empezar desde un estado conocido.

Se llama a la salida de e_mess_win (el manejador de mensajes emergentes) y e_opt_kst (el manejador de opciones y diálogos). Toda ruta de código que abra una ventana emergente y procese sus propios eventos de ratón debe llamar a e_mouse_flush al salir.

La prueba

Tres ventanas abiertas. Dos ficheros de código fuente y Messages. Arrastra cada ventana por su barra de título: suave, continuo, sigue la mano. Pulsa F9 : el compilador se ejecuta, aparece Messages. Arrastra Messages hacia abajo, arrastra una ventana de fuente hacia arriba, arrastra la tercera ventana. Suelta. Clica en otro sitio.
Todo funciona.

El registro del ratón muestra la prueba: gb=1 (bit 0 de g_mouse_buttons puesto) durante todo el arrastre, bstate=0x10000000 (REPORT_MOUSE_POSITION) para cada evento de movimiento, bstate=0x1 (BUTTON1_RELEASED) al final de cada arrastre. Sin pulsaciones huérfanas, sin estados atascados, sin caídas del modo 1002.

Trescientas líneas de eventos de ratón, y todas y cada una caen
en el patrón: pulsar, movimiento continuo, soltar. Pulsar, movimiento, soltar. La máquina de estados nunca se rompe. La máscara de bits nunca miente.

El we_mouse.c de Kruse: 1189 líneas de manejadores de arrastre, de
cambio de tamaño, de menú, de desplazamiento: nunca supo que nada de esto
pasaba. Llamaba a fk_mouse(g), recibía coordenadas limpias y movía ventanas. El mismo puntero a función, la misma abstracción, la misma arquitectura. De 1993 a 2026.

El reacomodo: ventanas que recuerdan dónde pertenecen

El arrastre no sirve de nada si el cambio de tamaño destruye tu disposición.

Cuando el terminal cambia de tamaño (SIGWINCH en ncurses, ConfigureNotify en X11), se vuelve a reservar el búfer de pantalla y todas las ventanas deben adaptarse.
El enfoque ingenuo: recortar las coordenadas a los nuevos límites. Funciona al reducir pero falla al crecer: las ventanas que se empujaron al borde durante una reducción nunca recuperan su espacio cuando el terminal vuelve a crecer.

El problema de fondo: deriva de proporciones. Si tienes una ventana en la fila 15 de
30, y el terminal se reduce a 20 filas, el truncamiento entero la pone
en la fila 10. Vuelve a crecer a 30: el truncamiento la pone en la fila 15. Parece
correcto. Pero hazlo 50 veces: reducir 1, crecer 1, reducir 1,
crecer 1: y el error de truncamiento acumulado va andando lentamente la
ventana hacia la parte de arriba de la pantalla.

Diseñamos un algoritmo de anclaje a los bordes:

Para cada coordenada de ventana, comprobar si tocaba un borde de la pantalla antes del cambio de tamaño. Una ventana cuyo borde inferior estaba en MAXSLNS - 2 (la última fila útil) está «anclada abajo»: su borde inferior sigue al borde de la pantalla. Una ventana cuyo borde superior está en la fila 1 está «anclada arriba»: se queda en la fila 1. Las coordenadas interiores: que no tocan ningún borde : se escalan de forma proporcional redondeando al más cercano:

int e_scale_y(int y, int old_h, int new_h)
{
    return (y * new_h + old_h / 2) / old_h;
}

El término old_h / 2 es la corrección de redondeo al más cercano. Sin él, 15 * 20 / 30 = 10, luego 10 * 30 / 20 = 15: parece reversible.
Pero 7 * 20 / 30 = 4 (truncado de 4.67), luego 4 * 30 / 20 = 6: has perdido una fila. Acumulado en muchos ciclos, la ventana se colapsa. Con redondeo: 7 * 20 + 15 = 155, / 30 = 5 (redondeado de 4.67), luego 5 * 30 + 10 = 160, / 20 = 8: sobrecorrige en uno, pero el error no se acumula. La ventana oscila una fila en vez de derivar de forma monótona.

Tres ventanas, Messages abajo, el terminal reduciéndose y
creciendo a lo bruto: cada ventana se queda donde debe. El editor
ocupa los dos tercios de arriba, Messages ocupa el tercio de abajo, y las
proporciones se mantienen.

Renderizado a prueba de límites

Un muro más entre el arrastre y el cuelgue: SCHIRM_INBOUNDS.

Cuando un diálogo es más ancho que el terminal (común durante una reducción
extrema), el bucle de renderizado intenta escribir celdas más allá de la
reserva de schirm. En 1993 los terminales no cambiaban de tamaño. En 2026, cada arrastre
del borde de un WM en mosaico provoca un cambio de tamaño. Escribir más allá de la reserva
corrompe el montón: cuelgues aleatorios segundos o minutos después.

#define SCHIRM_INBOUNDS(y, x) \
    ((y) >= 0 && (y) < MAXSLNS && (x) >= 0 && (x) < MAXSCOL)

Toda macro de acceso a schirm: e_pr_char, e_gt_char, e_gt_col, e_pt_col, e_gt_flags, e_pt_flags: comprueba los límites antes de
tocar memoria. Las escrituras fuera de límites se descartan en silencio. El
diálogo renderiza la parte que cabe; la parte más allá del borde del terminal es invisible. Haz crecer el terminal de vuelta y el diálogo reaparece
completo. Sin cuelgue, sin corrupción, sin estado que recuperar.

Es el mismo patrón que usan los emuladores de terminal modernos (st,
alacritty, foot): el modelo puede ser más grande que la vista, y la
vista recorta en silencio. El renderizador nunca escribe fuera de la vista.

Lo que Kruse sabía

Kruse escribió fk_t_mouse como un stub de cuatro líneas que devolvía cero.
Escribió fk_mouse como un puntero a función. Escribió 1189 líneas de
lógica de arrastre en we_mouse.c que nunca mencionaban X11 ni ncurses ni
GPM.

En 1993, el único backend de ratón que funcionaba era X11 (fk_x_mouse). Los ratones de terminal aún no existían: GPM era experimental, el soporte de ratón de xterm era rudimentario. Pero Kruse diseñó la separación de todos modos.
No podía saber que existiría el modo 1002, que ncurses
añadiría mousemask(), que GPM sería absorbido por ncurses, que las ventanas emergentes desincronizarían el estado de los botones. Pero diseñó una arquitectura que sobrevivió a todo eso.

Añadimos g_mouse_buttons, e_mouse_flush, e_relayout_windows y SCHIRM_INBOUNDS. Cuatro mecanismos, cada uno resolviendo un problema que no existía en 1993. Y we_mouse.c: el fichero que de verdad mueve las ventanas, las redimensiona, selecciona texto y desplaza contenido, no cambió en absoluto.

Eso no es suerte. Eso es diseño de Kruse.

El problema fantasma de X11

El renderizado X11 de Kruse usaba Xlib en crudo: XDrawImageString para el texto,
XDrawLine para los bordes de las ventanas, gestión manual de píxeles para todo.
Un array llamado extbyte rastreaba qué celdas tenían segmentos de línea de borde.
Cuando se cerraba una ventana emergente o el gestor de ficheros, los píxeles del borde persistían en la ventana X11: imágenes fantasma que no se iban.

El fallo era arquitectónico: extbyte es un array plano sin ningún concepto de apilamiento de ventanas. Una ventana detrás del editor podía tener sus flags de borde puestos, y e_x_refresh dibujaba diligentemente esos segmentos XDrawLine encima del contenido del editor. El arreglo que por fin funcionó: limpiar extbyte del interior de cada ventana de la pila en cada refresco. Pragmático, no elegante: pero atrapa todas las rutas de código.

Dennis Payne vio esto venir. En su fichero TODO escribió: «Usar
APIs de más alto nivel en vez de xlib y de la interfaz curses más baja».
Sabía que el renderizado a nivel de píxel era una trampa de mantenimiento.

El problema de la salida: de xterm a Messages

En el diseño original de Kruse, la salida del programa depurado iba al terminal (a través del cambio de pantalla smcup/rmcup). En modo X11, Payne añadió una ventana xterm: un script de shell que lanzaba el depurador dentro de un xterm mediante tuberías con nombre. El xterm robaba el foco del teclado, se cerraba antes de que pudieras leer la salida y dejaba xwpe sin respuesta si algo iba mal.

Unificamos la salida del programa para el modo terminal y el modo X11 en el búfer Messages: la misma ventana usada para los errores del compilador (F9). Ahora Ctrl-G P (pantalla de usuario) añade la salida del programa a Messages con desplazamiento completo. Ctrl-F9 Run para lenguajes interpretados (Python, LaTeX) captura la salida con popen() directamente en Messages, evitando xterm por completo.
Sin cambio de contexto, sin robo de foco, sin ventana que cerrar.

El mismo enfoque que Borland habría usado si su IDE hubiera funcionado en
UNIX: un panel de salida, varios usos, siempre accesible.

Siguiendo a st: la migración a Xft

La pieza final del rompecabezas X11 eran las fuentes. Kruse usaba
XDrawImageString, que es solo Latin-1. Sin caracteres acentuados, sin cirílico, sin emoji. En modo terminal, ncursesw se encarga de todo:
la pila de fuentes del emulador de terminal hace el trabajo. En modo X11, xwpe ES el renderizador.

Primero probamos un enfoque híbrido: Xft para algunos caracteres,
XDrawImageString para otros. Falló estrepitosamente. Los dos sistemas de renderizado usan métricas distintas, estado GC distinto, semántica de relleno de fondo distinta. Mezclarlos produjo fondos blancos, texto invisible y artefactos de borde. Cada arreglo parcial rompía otra cosa.

Dejé de parchear y tomé la decisión: implementar por completo la recomendación de Payne: ir a Xft para todo: en vez de parchear el híbrido. Fue más rápido que perseguir un arreglo parcial más, y fue la decisión correcta.

Estudiamos el código de st (suckless terminal)
y leímos cómo renderiza el texto. st usa Xft para el 100% de su renderizado: ni un XDrawImageString en ningún sitio. Los patrones que adoptamos:

  • XftFont vía fontconfig (monospace:size=10): cualquier fuente TrueType
  • XftDrawRect para el relleno de fondo (una llamada por celda)
  • XftDrawStringUtf8 para el texto (con fuente de reserva por carácter)
  • Caché de fuentes (array frc[]): búsqueda en fontconfig una vez por punto de código, en caché para siempre. El mismo patrón que st, la misma estructura de datos.
  • XftDraw sobre Pixmap: todo el renderizado va a un Pixmap fuera de pantalla, XCopyArea a la ventana al final. Cero parpadeo al cambiar de tamaño.
  • FC_COLOR para la reserva de emoji: fontconfig prefiere Noto Color Emoji para los puntos de código por encima de U+2600

La migración tocó we_xterm.c (renderizado), WeXterm.c (inicio de fuentes), WeXterm.h (campos Xft), configure.ac (detección de Xft) y Makefile.am (flags de enlazado). La ruta XDrawImageString original se conserva dentro de #else para los sistemas sin Xft.

Con libXft 2.3.5+ (distribuido en Debian desde 2022), el renderizado de emoji en color BGRA funciona de forma nativa. La misma llamada XftDrawStringUtf8 que renderiza hello también renderiza hello 🎉 con un glifo de emoji a todo color de Noto Color Emoji.

El problema de los caracteres anchos

Los emoji y los caracteres CJK ocupan dos columnas en pantalla (wcwidth() = 2).
Pero el búfer SCREENCELL de xwpe guarda un carácter por celda. Cuando un emoji ocupa las celdas 5 y 6, la celda 6 necesita saber que es la
«continuación» de la celda 5: si no, el cursor la trata como un
carácter independiente, borrar elimina solo la mitad y el renderizado dibuja un rectángulo de fondo sobre la segunda mitad.

Estudiamos cómo manejan esto st, vim, alacritty y libvterm. Todas las
implementaciones convergen en el mismo patrón: bits de flag en la celda.

Añadimos un campo flags a SCREENCELL con dos valores:

  • CELL_WIDE en la primera celda (el emoji en sí)
  • CELL_WIDE_SPACER en la segunda celda (la continuación)

El código de renderizado se salta las celdas espaciadoras. El cursor salta sobre ellas.
Borrar elimina las dos celdas. Los clics de ratón sobre una espaciadora se asignan al carácter ancho. El mismo patrón de mantenimiento de invariantes de st: cada operación de escritura comprueba los dos flags y limpia la celda compañera.

Este es un cambio transversal: toca unixmakr.h (la estructura), we_wind.c y we_progn.c (renderizado), we_edit.c (cursor y borrado) y we_mouse.c (asignación de clics). Pero el patrón es limpio y está bien probado en toda la industria.

Python, LaTeX y el patrón de los lenguajes de script

El ciclo 1.6.2 añadió soporte completo para lenguajes interpretados:

  • Python: resaltado de sintaxis de 35 palabras clave, F9 vía python3 -m py_compile,
    pdb como sexto backend de depuración (puntos de interrupción, pasos con F8, salto automático de
    los marcos internos de Python, captura de salida).
  • LaTeX: resaltado de sintaxis de 31 palabras clave, F9 vía pdflatex -interaction=nonstopmode -file-line-error, navegación por errores.
  • Ctrl-F9 Run detecta los lenguajes interpretados (comp_sw & 1) y ejecuta el intérprete directamente con popen(), capturando la salida a
    Messages. Sin xterm, sin gestión de subprocesos, sin robo de foco.

El patrón es extensible. Añadir un compilador nuevo lleva unas ~10 líneas en
we_prog.c (nombre del compilador, flags, patrón de error, extensiones de fichero). Si el compilador emite el formato file:line:column: (el estándar GNU desde 2013), funciona con cero configuración.

En v1.6.3 añadimos Perl (perl -c, resaltado de sintaxis de 46 palabras clave, ejecución con Ctrl-F9) y COBOL (cobc, resaltado de sintaxis de 50 palabras clave, modo GNU), lo que lleva el total a 9 compiladores por defecto.

Estado

  • v1.5.31: publicación de rescate (base de Payne + cherry-picks + parches de Debian)
  • v1.6.0: migración UTF-8/SCREENCELL, cambio de tamaño de terminal, teclas modernas
  • v1.6.1: autotools moderno (configure.ac + Makefile.am)
  • v1.6.2: la publicación que trajo xwpe de 1993 a 2026:
  • v1.6.3 (publicada en 2026-06, en Debian unstable): la publicación de usabilidad.

El fósil vive. Compila tu código, navega por tus errores, recorre
tus funciones paso a paso, te muestra la salida: con fuentes TrueType
suavizadas, emoji en color y UTF-8 del terminal a X11. Y ahora se siente bien bajo los dedos: Tab recorre los diálogos, los colores son legibles, Shift-Tab va hacia atrás, los errores siguen al cursor.

Fred Kruse construyó la arquitectura en 1993. Dennis Payne la mantuvo durante una década. Nosotros la trajimos a 2026, siguiendo los patrones de renderizado de st y las convenciones de caracteres anchos de la industria. El código pasó de las fuentes de mapa de bits y Latin-1 a fontconfig y Noto Color Emoji: y el diseño
central aguantó. Cada puntero a función, cada doble búfer, cada comparador de patrones que Kruse escribió sigue haciendo exactamente lo que fue diseñado para hacer.
Solo le dimos mejores herramientas con las que trabajar.

xwpe 1.6.3 está, a fecha de la publicación de esta entrada, en Debian unstable. El mismo binario está verificado de extremo a extremo en
gracias a una nueva adición: 134 pruebas de consola y 58 pruebas X11 sin pantalla pasan contra el paquete instalado, así que «compila» es ahora «funciona».

Y la parte que aún me llega: Fred Kruse, que escribió esa primera versión en 1993, ha estado en contacto. Quiere probarlo. Treinta y tres años después de que lo construyera, y dos décadas después de la última publicación, el autor original quiere ver su editor funcionar otra vez. Dennis Payne dio su bendición al traspaso; Kruse le dio su significado al rescate.

Cuando v1.7 llegue con soporte DAP (Debug Adapter Protocol): depuración de Rust, Go, JavaScript y C# desde una TUI de 1993: ese será el debut público del Halcón Milenario. Por ahora, vuelve a volar.

Antes y después:

Xwpe en 1.5.30. Sin soporte de UTF-8. Estilo antiguo de barras de desplazamiento.

Xwpe 1.5.30: sin soporte de UTF-8. Estilo antiguo de barras de desplazamiento

Xwpe, mostrando la salida de Ctrl-F9 en Messages

XWPE 1.6.3, mostrando la salida de Ctrl-F9 en la ventana Messages

Xwpe 1.6.3 con soporte de UTF-8

Probando XWPE en Wayland con soporte de UTF-8, abriendo un fichero con «emojis». «El problema de la celda ancha»

Etiquetas

  • #debian
  • #software-archaeology
  • #xwpe