Un gestor de tareas de escritorio, iOS y Android escrito en
raylang (probado con 1.4.0). Ventana/webview del
sistema + puente IPC de std/ui + persistencia con std/kv. El mismo programa
raylang (src/) corre en macOS, Linux, iOS y Android. Pensado como banco de
pruebas de las características del lenguaje.
┌─────────────────────────┐ window.ray.send(json) ──────▶ ┌───────────────────────┐
│ Ventana nativa │ (IPC, std/ui) │ Backend raylang │
│ std/ui + webview │ │ event loop (1 fibra) │
│ (assets/index.html, │ ◀── eval_js(rayRender(tasks)) ── │ handle_message() │
│ app.css, app.js) │ │ → std/kv (abierto 1x)│
│ │ GET / , /app.* (assets) ────▶ │ web (solo estáticos) │
└─────────────────────────┘ └──────────┬────────────┘
std/kv (store, 1 clave/tarea) ▼
~/Documents/RayDesk/tasks.kv
- UI:
std/ui.open()abre una ventana nativa que carga la página; la fibra principal corre el event loop (ui.next_event) y cierra el proceso al cerrar la ventana. - Datos (IPC,
std/ui): la página llamawindow.ray.request(cmd)(p. ej.{"cmd":"add","title":…}) y recibe una Promise que resuelve con la lista actualizada (JSON). En raylang llega como evento"message";handle_messagelo decodifica conui.as_request, muta el storekvy resuelve la Promise conui.reply(window, id, json). Sin API HTTP. (Fallback:window.ray.sendfire-and-forget + push poreval_js(window.rayRender…), para shells antiguos y las actualizaciones iniciadas por menús.) - Assets:
web.static_embedded(app, "/", "assets")sirve la página (bundle-safe, con ETag/304/Range); el servidorwebcorre en otra fibra (spawn+web.listen_on) y solo sirve estáticos. El puerto se obtiene sin carrera connet.tcp_listen(…, 0)+net.local_port. - Look: UI moderna con Tailwind CSS (build purgado:
assets/app.csssolo contiene las clases realmente usadas enindex.html/app.js, ~12 KB minificado). Botones azules con estados hover/active/focus-ring, tarjetas redondeadas con sombra, modo oscuro porprefers-color-scheme(variantesdark:). Regenerar tras tocar clases:tailwind/build.sh(oray devpara el raylang; el CSS se rehace con el script). Fuentes entailwind/(config + input); la salidaassets/app.cssva embebida. - Menús (
std/ui): "Acerca de RayDesk" va en el menú de app (macOS: el primero, en negrita) víaui.app_menu, que además lo retitula a "RayDesk" incluso bajoray run. Más menús custom "Archivo" (Nueva tarea ⌘N, Exportar tareas… ⌘E, Recargar ⌘R) y "Tarea" (Limpiar completadas ⌘K) conui.menu. El click llega como evento"menu"contag; las acciones manejan el frontend víaui.eval_js(reusan los handlers de la página, así la UI queda sincronizada), "Exportar" abre el diálogo nativoui.save_file, y "Acerca de RayDesk" (tag: "role:about") abre el panel About nativo de macOS relleno conui.set_about(nombre, versión, descripción, ©); en Linux cae en un modal informativo en la webview. Los menús estándar App/Edit (⌘Q, portapapeles, undo) se instalan solos. - Persistencia:
std/kvcon una clave por tarea (la clave es elid, unuuid_v7ordenable por tiempo, asíkeys()viene en orden de creación) y guardado atómico (temp + rename). El store se abre una sola vez y lo posee la fibra del event loop (el único que lo toca), así que no se reabre por mensaje ni hay carrera con la fibra del servidor. Se guarda en~/Documents/RayDesk/tasks.kv(en iOS la raíz del contenedor no es escribible;Documents/sí y persiste).std/fsse usa solo para crear ese directorio (fs.mkdir). Para acceso concurrente entre fibrasstd/kvofrece la forma actor (open_shared) y ops atómicas (incr,set_if), aquí no necesarias.
src/
├── model.ray # Todo + JSON (una tarea <-> objeto; lista para la API)
├── store.ray # repositorio std/kv (list/add/toggle/remove/clear_done) + apply(cmd)
└── main.ray # web static + IPC (window.ray → std/kv) + std/ui event loop
tests/
├── model_test.ray # (de)serialización y field_of
└── store_test.ray # CRUD del store + dispatch de comandos (store.apply)
assets/
├── index.html # utilidades Tailwind
├── app.js # frontend: IPC (window.ray.request/send + rayRender) + Tailwind
└── app.css # GENERADO por Tailwind (purgado) — no editar a mano
tailwind/
├── tailwind.config.js # content: index.html + app.js
├── input.css # @tailwind base/components/utilities
└── build.sh # regenera assets/app.css
raydesk-ios/ # shell iOS (Xcode) generado por `ray bundle --ios`
├── Shell/ # AppDelegate + SceneDelegate (UIScene) en Objective-C
├── App.xcconfig # build settings + firma (DEVELOPMENT_TEAM, respetada por el bundle)
├── raydesk.xcodeproj/
└── libs/ · libs-sim/ # staticlib del programa (regenerable, gitignored)
raydesk-android/ # shell Android (Gradle) generado por `ray bundle --android`
├── app/src/main/ # MainActivity + RayBridge (Java), Manifest, res/
│ └── jniLibs/<abi>/ # libray_app.so (cdylib del programa; regenerable, gitignored)
├── app/build.gradle · settings.gradle · gradle.properties
└── README.md # compilar/instalar el APK
ray add web@^0.2.0 # (ya en ray.toml) descarga web + net del registro
ray run # abre la ventana (dev: assets en vivo desde disco)
ray dev # igual, con recarga al guardar cambios
ray test # corre los @test de tests/ (13: model + store)
ray build --native --release
ray bundle --name RayDesk --id org.rayala.raydesk # empaqueta la .app / .desktopVerificado con el MCP de raylang usando el parámetro
path(contexto de proyecto: resuelve módulos y dependencias) y con corridas headless: los assets se sirven bien (web+RAY_UI_BACKEND=headless), y el camino IPC se probó inyectando un mensaje conRAY_UI_MSG='{"cmd":"add","title":"…"}'— la tarea llega ahandle_message, se guarda en el storekv(verificado abriéndolo) y persiste entre lanzamientos, con UTF-8. La ventana real se abre conray run.
El mismo src/ corre en iOS: ray bundle --ios compila el programa raylang a
un staticlib y genera un shell Xcode en raydesk-ios/. Dentro de la app, el
programa arranca su webserver embebido en 127.0.0.1 y ui.open(title, url) carga
esa URL en un WKWebView — el mismo frontend (assets/) y la misma API que en
escritorio. Los eventos de ciclo de vida llegan por ui.next_event() como
kind="lifecycle", tag="background"/"foreground".
- Persistencia: en iOS la raíz del contenedor de la app no es escribible, por
eso el store
kvvive en~/Documents/RayDesk/tasks.kv(escribible y persiste entre lanzamientos). Versrc/store.ray. - UI:
raydesk-ios/Shellusa el ciclo de vida UIScene (SceneDelegatealoja elWKWebViewy conecta los handlers destd/ui).
# Regenera el staticlib + proyecto Xcode (respeta la firma existente)
ray bundle --ios
# --ios-target device|sim # construye solo un lado (conserva el otro .a)
# Simulador (sin firma):
xcodebuild -project raydesk-ios/raydesk.xcodeproj -target raydesk \
-sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO
# luego: xcrun simctl boot <device> && install && launch
# Dispositivo: abre raydesk-ios/raydesk.xcodeproj en Xcode y compila.- Firma: el
DEVELOPMENT_TEAMse fija enraydesk-ios/App.xcconfigyray bundle --ioslo respeta (no lo regenera) — verROADMAP.md#9. - Los
libs//libs-sim/(staticlibs, ~15 MB) están gitignored: se regeneran con el comando de arriba.
El mismo src/ corre en Android: ray bundle --android compila el programa
raylang a un cdylib (libray_app.so) y genera un shell Gradle en
raydesk-android/. La MainActivity (Java) aloja un WebView, arranca el
programa (que sirve la UI en 127.0.0.1) y expone el mismo puente IPC que
escritorio/iOS: window.ray.send(text) → evento "message" (RayBridge); los
eventos lifecycle llegan en onPause/onResume. stdout/stderr van a logcat
con tag ray.
- Persistencia:
std/kv/std/fsescriben en el directorio privado de la app (scoped storage; el cwd no es escribible). - Red:
network_security_config.xmlpermite cleartext a127.0.0.1/localhost(el webserver embebido).
ray bundle --android # genera el shell + libray_app.so
# --android-abi arm64|x86_64|all # construye un ABI (conserva el otro .so)
cd raydesk-android
gradle assembleDebug # requiere Android SDK + JDK 17+
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb logcat -s ray # stdout/stderr del programa- ABI: el
app/build.gradlefijaarm64-v8a; para el emulador x86_64 usaray bundle --android --android-abi all(si no:INSTALL_FAILED_NO_MATCHING_ABIS). local.properties(ruta del SDK) y todo lo de build (.gradle/,build/, el APK ylibray_app.so) están gitignored — se regeneran. Detalles enraydesk-android/README.md.
paquete web 0.2.0 (Tier-2: listen_on, static_embedded) · std/ui
(ventana, puente IPC window.ray.request/send + as_request/reply,
eval_js, app_menu, set_about, menu, save_file, eventos) · std/net ·
std/kv (store con guardado atómico) · std/fs (mkdir, export) ·
std/json · std/uuid (uuid_v7) · std/time · módulos + pub ·
concurrencia (spawn) · struct/enum (uso cross-módulo) · pattern matching ·
Result/Option + ? · closures (map) · @test.
ROADMAP.md— lo que se echó de menos y su estado (varias cosas ya resueltas:listen_on,static_embedded,ray_docde módulos, docs).BUGS.md— posibles bugs con repro (todos resueltos a día de hoy).