synsema
ES Español

Distribuí tu app web como app de escritorio — un binario, una ventana del navegador, tu ícono, sin consola

synsema build --serve --no-console --icon convierte la misma app renderizada en el servidor en una app de escritorio de doble clic — un .exe con tu ícono en Windows, un .app en macOS, una carpeta lanzadora en Linux. La ventana es el navegador que el usuario ya tiene, y el proceso termina cuando se cierra la última. Sin Tauri, sin webview, sin segundo código.

Una app de escritorio es una app web que abre en su propia ventana, lleva un ícono, no muestra consola y se va cuando la cerrás. Synsema llega ahí sin toolkit nativo: el servidor es el programa, la ventana es el navegador que el usuario ya tiene, y synsema build hace la cirugía sobre el binario. El backend, los agentes y los secretos se quedan en el proceso, exactamente igual que en un servidor.

La forma§

Cuatro decisiones, todas en el programa. Esta es la rama de Windows de la receta; las de macOS y Linux (open -a "Google Chrome" --args --app=…, google-chrome --app=…, xdg-open) se eligen solas con platform(), y synsema init --desktop te escribe todo.

require serve(8123)
require exec("cmd")             -- Windows: Edge in --app= mode, no address bar
require file.read("index.html")
require time

let started be now()

task maybe_quit()
    when state_get("windows", 0) > 0
        give nothing
    let closed be state_get("last_close", nothing)
    when closed != nothing and now() - closed > 3        -- last window closed 3 s ago
        shutdown("window closed")
    when closed == nothing and now() - started > 30       -- no window ever opened
        shutdown("no window opened in 30 s")

serve on 8123
    bind "127.0.0.1"                                      -- local only: the LAN never sees it
    route "GET /"
        give render("index.html", {"title": "My app"})
    route "GET /ws"                                       -- one socket per open window
        socket
            state_incr("windows")
            while true
                let ev be ws_recv(socket, 30)
                when ev != nothing and ev["type"] == "close"
                    stop
            state_incr("windows", -1)
            state_set("last_close", now())

cron_every(2, maybe_quit)
run("cmd", ["/c", "start", "", "msedge", "--app=http://127.0.0.1:8123/"])

1. bind "127.0.0.1" es una cláusula del bloque serve, así que la intención viaja con el programa. 2. La ventana es el navegador en modo app, lanzado con run() bajo require exec — las mismas reglas de capacidad que cualquier proceso hijo. Edge y Chrome son de instancia única, así que mirar el proceso del lanzador mentiría; el socket es la verdad: hay ventana ⇔ hay WebSocket (la página abre uno con new WebSocket("ws://" + location.host + "/ws")). 3. shutdown() corre el mismo drenaje ordenado que Ctrl-C — listener cerrado, trabajo en vuelo drenado, cron y agentes detenidos, exit 0. Es idempotente, rechaza un secret como motivo y bajo synsema run es un error claro, nunca una salida silenciosa. 4. La guarda de 30 segundos importa: un proceso sin consola cuyo navegador nunca abrió quedaría invisible para siempre.

Construilo§

synsema build desk.syn -o desk --serve --no-console --icon icon.svg                # Windows: desk.exe
synsema build desk.syn -o desk --serve --icon icon.svg --bundle                    # macOS: desk.app/
synsema build desk.syn -o desk --serve --icon icon.svg --bundle --name "My App" --id com.example.myapp
synsema build desk.syn -o desk --serve --icon icon.svg --bundle --engine-binary ./synsema-linux-x86_64   # desk/ + install.sh

Las banderas miran el motor que se está envolviendo, nunca la máquina que construye. -o desk se vuelve desk.exe cuando el motor es un ejecutable de Windows. --no-console da vuelta dos bytes en el encabezado del ejecutable para que un doble clic no abra ninguna consola. --icon toma un .svg, un .png o un .ico: en Windows se convierte en una sección de recursos que leen el Explorador, la barra de tareas y los accesos directos; en macOS en un .icns dentro del .app; en Linux en los PNG al lado del lanzador. --bundle escribe My App.app/ (con LSUIElement, así el proceso corre como agente: sin ícono en el Dock, sin Terminal) o desk/ con un archivo .desktop y un install.sh de diez líneas que instala bajo ~/.local, sin root. La línea built … dice exactamente qué se hizo:

built desk.exe (15 files, 38333285 bytes) · serve · bind 127.0.0.1 · no-console · icon 16/32/48/256

Qué hace cada sistema operativo con esto§

  • Windows — verificado en vivo: el doble clic abre la ventana de app de Edge, el Explorador

muestra tu ícono y cerrar la ventana termina el proceso con exit 0 en unos cinco segundos. Como Chromium trata a 127.0.0.1 como contexto seguro, una página con manifiesto y service worker además se instala desde el menú de Edge, con su propia entrada en el menú Inicio y su identidad en la barra de tareas — los mismos archivos que trae el andamiaje PWA.

  • macOS — verificado por CI en Apple Silicon: el .app construido con el motor real se lanza

con open, sirve y se detiene, y el bundle adosado mantiene válida la firma ad-hoc del enlazador. Descargado sin firma de Developer ID se encuentra con Gatekeeper como cualquier app sin firmar (clic derecho → Abrir); un .app construido localmente abre directo.

  • Linux — la estructura de carpetas y el install.sh están verificados por tests:

Terminal=false en la entrada .desktop es el --no-console de Linux. Chrome y Chromium dan una ventana de app; un escritorio con solo Firefox recibe una pestaña.

Qué no es§

Sin bandeja del sistema, sin menús nativos, sin .dmg/.msi/AppImage — los empaquetadores del ecosistema toman un .app, un .exe o una carpeta, y eso es exactamente lo que produce --bundle. El puerto es fijo, así que elegí uno poco común. Y sin consola no hay log salvo que lo escribas (append_file bajo file.write); desk.exe --engine version desde una terminal sigue imprimiendo, y una salida que nadie lee es un cierre silencioso, no un panic.

Arrancá desde el andamiaje§

synsema init myapp --desktop && cd myapp
synsema serve desk.syn        # the window opens; close it and the process ends
synsema serve app.syn         # the same app as a site / PWA on :8080

--desktop escribe el andamiaje PWA con su API en api.syn — un grupo export routes que montan las dos entradas, con los límites de tasa por ruta incluidos — más desk.syn y public/desk.js. Una API, dos entradas, tres formas de distribuir.

Probalo: instalá Synsema (0.6.19 o más nuevo), corré las tres líneas de arriba y después leé Tu app en el escritorio para cada bandera y las notas honestas por sistema operativo. Podés probar el lenguaje primero en try.synsema.org.