Saltar al contenido
Development

Agregar un menú nativo de macOS a Greenhouse rompió Cmd+Q, y mi harness de pruebas tampoco pudo presionar Cmd+N

Por Victor Da Luz
rusttaurimacosdev-loggreenhouse

Greenhouse (mi gestor de proyectos creativos, Tauri v2 + Svelte) no tenía ningún menú nativo de macOS, solo lo que Tauri muestra por defecto. Sin menú File, sin atajos de teclado más allá de un input de renombrado. La solución parecía un par de horas de Rust directo: construir un menú, conectar cinco atajos de teclado, listo.

No fue tan simple.

El menú por defecto no es una base sobre la cual construir

Supuse que llamar a app.set_menu(my_menu) agregaría mis elementos personalizados sobre lo que Tauri ya muestra. No es así. En el momento en que se establece cualquier menú, el menú automático por defecto desaparece por completo, no se combina, se reemplaza.

Esto lo descubrí construyendo exactamente lo que pedía el issue: un menú File con “New Idea” y un menú View con Board/Vault/Harvest. Se veía completo. Entonces pensé en qué estaba reemplazando y caí en la cuenta: el menú por defecto es también donde viven Cmd+Q, Cmd+H, y Undo/Cut/Copy/Paste/Select All para cada campo de texto. Mi input de renombrado habría quedado muerto en el momento en que esto se publicara, y también la forma normal de cerrar la app.

La solución fue construir el menú completo, no solo la parte nueva: un submenú App (About/Services/Hide/Quit), un submenú Edit (Undo/Redo/Cut/Copy/Paste/Select All), y después mis cosas nuevas de verdad. SubmenuBuilder tiene métodos de una línea para todo esto (.quit(), .select_all(), etc.), así que no fue mucho código. La trampa estaba en no darme cuenta de que lo necesitaba hasta que me detuve y me pregunté qué estaba a punto de tirar.

Después mi propio harness de pruebas no pudo verificar nada de esto

Manejo la interfaz de Greenhouse para verificación a través de un plugin de WebDriver, IPC real, estado real de Svelte, sin mocks. Excelente para hacer clic en botones. Resultó no tener ningún camino hacia una barra de menú nativa ni hacia una combinación de teclas física: solo envía ejecución de JS y llamadas de IPC dentro del webview, nada a nivel de entrada del sistema operativo.

Eso ya lo esperaba de entrada. Lo que sorprendió fue una segunda grieta, distinta, en el mismo territorio. El plugin también expone un endpoint de “actions” según el estándar W3C que se supone simula presiones de tecla, eventos keyDown/keyUp enviados correctamente a través del protocolo WebDriver, no un truco de JS. Supuse que al menos eso funcionaría para probar el nuevo manejador de Escape-para-volver. No funcionó, enviar un Escape a través de ese endpoint no tuvo ningún efecto en la app, confirmado al comparar contra un evento sintético de DOM que sí funcionaba de inmediato. Ya había topado con una versión exacta de este mismo problema antes, en una sesión anterior, y solo recordé por qué a mitad de volver a depurarlo.

La solución se dividió en dos niveles, una vez que dejé de intentar que una sola herramienta hiciera ambos trabajos. Para probar la lógica del propio manejador de keydown en JS: despachar un KeyboardEvent sintético directo a la página, a un listener de JS no le importa si un evento viene de una fuente confiable, así que esto sí ejercita el manejador de verdad. Para probar el enlace entre el menú y el evento del frontend: saltarse el menú y la tecla por completo, e invocar directamente el mismo comando de bajo nivel de eventos de Tauri que emite mi código de Rust (plugin:event|emit). Esto recorre el sistema real de eventos de punta a punta y demuestra que el listener del frontend funciona, sin necesitar un clic de menú nativo que no había forma de simular.

Lo que no pude simular, y no lo intenté: un evento sintético jamás va a disparar la acción por defecto propia de un navegador, como el cierre de un <dialog> nativo al presionar Escape. Eso solo se dispara para un evento “confiable”, entrada real de un usuario. Así que cerrar un diálogo en un script de pruebas necesita un clic real sobre su propio botón de cierre, no una tecla falsa. Y los atajos reales de ⌘N a ⌘4 todavía necesitan una mano humana real sobre un teclado real antes de dar por terminada la funcionalidad, no hay forma de evitar ese último paso, y dejé de buscarle la vuelta.

El patrón detrás de las dos cosas

Las dos sorpresas vinieron del mismo error: suponer que la descripción del trabajo de una herramienta cubría más de lo que en realidad cubre. “Establece el menú de la app” sonaba aditivo. “Simula presiones de tecla” sonaba literal. Ninguna de las dos suposiciones era falsa en la superficie, los nombres de las API son honestos, simplemente no me había preguntado qué implicaba la ausencia de una garantía explícita. La solución para ambas fue la misma: detenerse, listar qué provee en realidad el comportamiento actual (el que funciona, el que es por defecto), y verificar que el reemplazo lo cubra todo antes de asumir que lo hace.

Lecturas relacionadas

Development

La corrección que me convencí de no hacer

Una recomendación de 'no construir esto' basada en dos suposiciones sin verificar, y la corrección de una sola línea en la política de activación de Tauri que estuvo ahí todo el tiempo.

Leer
Development

Dos notas que nadie vio jamás

Greenhouse guardaba fielmente una nota de captura y una nota de traspaso, y no mostraba ninguna de las dos: una no tenía campo en el tipo de wire, la otra tenía un comando funcional que nadie llamaba.

Leer