Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a3aae762 | |||
| ff28b3c5e5 | |||
| 6cb63b266b | |||
| 6facc3c8ed | |||
| b6330a22b4 | |||
| de97c34965 | |||
| be127def7a | |||
| 031a164f47 | |||
| 69a01460a3 | |||
| 05f916a7d1 | |||
| 2545d3108d | |||
| d8866897ea | |||
| 608a37749b | |||
| 12e465037d | |||
| 23e128355b | |||
| 9b00740796 | |||
| 794d3e932e | |||
| 3d7f92ccd7 | |||
| c61ea22097 | |||
| a072538465 | |||
| 97bea4c99f | |||
| 786a22debe | |||
| 5f4d260301 | |||
| fa8dfd6ed3 | |||
| 0381585745 | |||
| dac1fc71ad | |||
| 05f2e54ed1 | |||
| 4c7d716c0c | |||
| 1fdbd358c8 | |||
| ac894390f9 | |||
| 906e5f639c | |||
| ef3d698c4b | |||
| 2588673caf | |||
| fa2bedf851 | |||
| b5032de1e3 | |||
| 7f7ba1fef9 | |||
| 24218447e3 | |||
| 9db79617eb | |||
| a908d8420c | |||
| d4c31d70b7 | |||
| 529287f01d | |||
| 8235391add | |||
| 7a53fea13c | |||
| cb5d83b1f7 | |||
| 38e95e0620 | |||
| b17149c528 | |||
| 12e3b1d4d0 | |||
| 42fee6fcbc | |||
| 07ab095d42 | |||
| b1be94e7c4 | |||
| 54f51ad2f8 | |||
| bdd088b89e | |||
| 60553bec44 | |||
| e100c5acff | |||
| 71ebcea899 | |||
| 0422780dd9 | |||
| 08ec152b7f | |||
| 9fe9c8319e | |||
| d40382ad01 | |||
| 28c2e6af18 | |||
| 6a29152feb | |||
| be4de986be | |||
| 8fcc4dc067 | |||
| c715ee2dee | |||
| 89fd927f76 | |||
| 2ad11b53c3 | |||
| 5edd8de917 | |||
| 89117f47cc | |||
| d85bb3819e | |||
| 8787b0566a | |||
| 8fc1d99ddf | |||
| d1b700eb2b | |||
| 922ad1f86b | |||
| a388ab14b7 | |||
| 4bcdc88c83 | |||
| 19d04a2766 | |||
| 089afccbbc | |||
| 8f946b449c | |||
| c4d1dd7bc2 | |||
| 2035e6dfa3 | |||
| ac787fb3b3 | |||
| 2523652e22 | |||
| 6e30dc2d72 | |||
| cfb37af0cf | |||
| 267e54f974 | |||
| dacfbad124 | |||
| 3454b8ba1e | |||
| 5c462e6d30 | |||
| e94023d8af | |||
| 93856cd4d7 | |||
| 65c88ccf25 | |||
| 8fc26b0c0c | |||
| 900cd19e49 | |||
| 6a0781c0a8 | |||
| 7695dbf3ce | |||
| 43e6b32e15 | |||
| 121aa130af | |||
| 277b2ab8d2 | |||
| 36d6645e7f | |||
| 5fdae159f0 | |||
| 208dc15569 | |||
| a7e9662fad | |||
| 0ada1050f7 | |||
| bce4abd8cc | |||
| 4f31146533 | |||
| 4c3b02c337 | |||
| cb0c5c22a8 | |||
| efcf6f825f | |||
| e2272c0ed3 | |||
| 1c6aafbc24 | |||
| 5770759f2e | |||
| 11d8bcb4af | |||
| 823eff0343 | |||
| 54de7849c5 | |||
| 968255f4cd | |||
| ed4d5ffe4c | |||
| ec111a2e27 | |||
| 363f401b75 | |||
| 15e45ace9b | |||
| e77a32f4d6 | |||
| 762bf95dfd | |||
| 3339fb009c | |||
| 663fd6e38a | |||
| 758f61f52a | |||
| 634a43006a | |||
| b3278bdb0c | |||
| f250fd0765 | |||
| 82ba0b7a1a | |||
| fa65f20206 | |||
| 7867d6830e | |||
| 2728698f5e | |||
| 47c49acd47 | |||
| 3902763daa | |||
| 28ee1ae260 | |||
| 4df7478b71 | |||
| 321e6f8e27 | |||
| b052c41e3c | |||
| 4d9ce11b1e | |||
| e52b3fb572 | |||
| 85156c1509 | |||
| 71a4068d12 | |||
| b95383b01a | |||
| d3cdbb2d2d | |||
| ab56b35e9f | |||
| c56ef25546 | |||
| b13486e240 | |||
| ab2b8f6638 | |||
| ab1cf9998a | |||
| 6588d0ed6c | |||
| eba629a806 | |||
| 86d06da020 | |||
| 0d1e378e42 | |||
| ec83dd7fb7 | |||
| e187ab4cd3 | |||
| 78fbde12b4 | |||
| bb21c28eb1 | |||
| 8a15b812f9 | |||
| af3be482a9 | |||
| 5369c307e4 | |||
| 2bd6aaad02 | |||
| a9e880240d | |||
| b17ac8c67a | |||
| ee27108f6c | |||
| 7b5c583a98 | |||
| b3b44e2c72 | |||
| 5ede927d34 | |||
| 58e72c3d08 | |||
| 751416252f | |||
| fb3091d995 | |||
| 90bffec3f1 | |||
| ae81399857 | |||
| b4ddab9c06 | |||
| 5230d42b11 | |||
| c07356d8bc | |||
| 84f94574f1 | |||
| 97a10aa173 | |||
| 30669f970b | |||
| 97a725b870 | |||
| 8cd34bf173 | |||
| fb2f3a2d01 | |||
| 782244b743 | |||
| fe221869d2 | |||
| f8cb80d867 | |||
| 6c3a86fbec | |||
| f9c4bf594e | |||
| 13d2ae71fb | |||
| 799dcef22e | |||
| be61ddb6eb | |||
| 6dfd9e62ac | |||
| 4719f7c9f9 | |||
| 2dd8ff139e | |||
| 8821d34bd5 | |||
| b31f988833 | |||
| 8204852e3a | |||
| 51398f89cf | |||
| c69eec794f | |||
| d2e0cf4830 | |||
| f2455b0eca | |||
| 4caf92482f | |||
| 3e335df298 | |||
| f57c61fe3e | |||
| 3e2777540f | |||
| e736587857 | |||
| b7181da7d9 | |||
| fdf820edbb | |||
| 7dc533dbbf | |||
| 93e003be0d | |||
| df8f92fbb0 | |||
| 5b9d8107fc | |||
| 22a0ae8c58 | |||
| b08d5cbe0a | |||
| b4be5e1c72 | |||
| 0740d2e2af | |||
| 9d8f45a9f8 | |||
| 05ccab64f2 | |||
| be99ac3bbb | |||
| 37ea535cb7 | |||
| 3bfb42f1cc | |||
| 0fde2aa273 | |||
| 2540e74046 | |||
| 90607bd7c0 | |||
| 2b2a92a72b | |||
| 0f2f1033eb | |||
| 8250ecbc0f | |||
| 69cee95481 | |||
| 7c38a8af4e | |||
| cf337c88d7 | |||
| 2b340fdf40 | |||
| b975dc7919 | |||
| 737ae5a696 | |||
| 9e7fa17411 | |||
| e3980d005f | |||
| 3f8a3ea4b6 | |||
| 71f6cf1306 | |||
| 639381fd94 | |||
| ea079a0b23 | |||
| d0a175a90a | |||
| 4e27065a15 | |||
| c1c136954e | |||
| ad9781c2ee | |||
| ced5853154 | |||
| 494fb7c0bc | |||
| 781a310c8d | |||
| 649ca02d4d | |||
| cbca62f8f1 | |||
| f46c7b435f | |||
| cba61e3549 | |||
| e2833a20c4 | |||
| bbfa44ff35 | |||
| ed651d6ac5 | |||
| cd3b41a401 | |||
| d1727b1374 | |||
| 191e6b06e1 | |||
| e0ad7315be | |||
| c2d6c15138 | |||
| b9a6cd33fd | |||
| 1da4ee11d7 | |||
| 6884b3f8cb | |||
| 353e0bbb43 | |||
| 05886022e0 | |||
| 0e13c35f3e | |||
| 1e01dc27a5 | |||
| 27603c906d | |||
| dc8554d123 | |||
| 0042fe3f1f | |||
| 94ea0eaa53 | |||
| 590572b5bb | |||
| 370a593ad8 | |||
| 4528e08e04 | |||
| b75e22fa91 | |||
| 8cd8003dd5 | |||
| b7d9d7abd9 | |||
| cba3a9dd6e | |||
| af5d4a1f22 | |||
| f8a2547b45 | |||
| 545dd59c72 | |||
| 67c0fcad11 | |||
| 848fc7a072 | |||
| b83d40a833 | |||
| 3fc6dcfa72 | |||
| e570c6ca6f | |||
| 550c98f275 | |||
| 86fb6ae20b | |||
| 4619ba3a2b | |||
| eac8c58974 | |||
| d341004f59 | |||
| 9b12958660 |
@@ -4,3 +4,6 @@ Cargo.lock.bak
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.claude/
|
.claude/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
|
# Artefacto de runtime del event-log de nakui-ui
|
||||||
|
nakui-ui-state.jsonl
|
||||||
|
|||||||
+11
-5056
File diff suppressed because it is too large
Load Diff
Generated
+3159
-1248
File diff suppressed because it is too large
Load Diff
+311
-156
@@ -2,44 +2,71 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# core/ — Init y compat (arje absorbido)
|
# protocol/ — Contratos canónicos + routing entre módulos
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/core/brahman-card",
|
"crates/protocol/brahman-card",
|
||||||
"crates/core/brahman-card-wit",
|
"crates/protocol/brahman-card-wit",
|
||||||
"crates/core/brahman-cards",
|
"crates/protocol/brahman-cards",
|
||||||
"crates/core/brahman-handshake",
|
"crates/protocol/brahman-handshake",
|
||||||
"crates/core/brahman-broker",
|
"crates/protocol/brahman-broker",
|
||||||
"crates/core/brahman-admin",
|
"crates/protocol/brahman-admin",
|
||||||
"crates/shared/brahman-sidecar",
|
"crates/protocol/brahman-sidecar",
|
||||||
"crates/shared/brahman-net",
|
"crates/protocol/brahman-net",
|
||||||
"crates/shared/ente-incarnate",
|
"crates/protocol/brahman-dht",
|
||||||
"crates/core/ente-card",
|
"crates/protocol/brahman-card-discovery",
|
||||||
"crates/core/ente-bus",
|
"crates/protocol/brahman-ssh-multiplex",
|
||||||
"crates/core/ente-cas",
|
"crates/protocol/brahman-auth",
|
||||||
"crates/core/ente-kernel",
|
"crates/protocol/arje-card",
|
||||||
"crates/core/ente-soma",
|
|
||||||
"crates/core/ente-wasm",
|
|
||||||
"crates/core/ente-snapshot",
|
|
||||||
"crates/core/ente-brain",
|
|
||||||
"crates/core/ente-zero",
|
|
||||||
"crates/core/ente-echo",
|
|
||||||
"crates/core/ente-policy-provider",
|
|
||||||
"crates/core/ente-logind-compat",
|
|
||||||
"crates/core/ente-hostnamed-compat",
|
|
||||||
"crates/core/ente-timedated-compat",
|
|
||||||
"crates/core/ente-localed-compat",
|
|
||||||
"crates/core/ente-journald-compat",
|
|
||||||
"crates/core/ente-resolved-compat",
|
|
||||||
"crates/core/ente-polkit-compat",
|
|
||||||
"crates/core/ente-machined-compat",
|
|
||||||
"crates/core/ente-tmpfiles-compat",
|
|
||||||
"crates/core/ente-systemd1-compat",
|
|
||||||
"crates/core/ente-notify-compat",
|
|
||||||
"crates/core/ente-binfmt-compat",
|
|
||||||
"crates/core/ente-timer-compat",
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/semantic_dht/ — DHT semántico (minga absorbido)
|
# init/ — PID 1 + encarnación Linux (arje)
|
||||||
|
# ============================================================
|
||||||
|
"crates/init/arje-zero",
|
||||||
|
"crates/init/arje-kernel",
|
||||||
|
"crates/init/arje-soma",
|
||||||
|
"crates/init/arje-snapshot",
|
||||||
|
"crates/init/arje-incarnate",
|
||||||
|
"crates/init/arje-absorb",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
||||||
|
# ============================================================
|
||||||
|
"crates/runtime/arje-bus",
|
||||||
|
"crates/runtime/arje-cas",
|
||||||
|
"crates/runtime/arje-wasm",
|
||||||
|
"crates/runtime/arje-brain-rules",
|
||||||
|
"crates/runtime/arje-brain-cognitive",
|
||||||
|
"crates/runtime/arje-brain-audit",
|
||||||
|
"crates/runtime/arje-brain",
|
||||||
|
"crates/runtime/arje-echo",
|
||||||
|
"crates/runtime/sandokan-lifecycle",
|
||||||
|
"crates/runtime/sandokan-core",
|
||||||
|
"crates/runtime/sandokan-local",
|
||||||
|
"crates/runtime/sandokan-daemon",
|
||||||
|
"crates/runtime/sandokan-remote",
|
||||||
|
"crates/runtime/sandokan",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
|
# ============================================================
|
||||||
|
"crates/compat/arje-compat-common",
|
||||||
|
"crates/compat/arje-policy-provider",
|
||||||
|
"crates/compat/arje-logind-compat",
|
||||||
|
"crates/compat/arje-hostnamed-compat",
|
||||||
|
"crates/compat/arje-timedated-compat",
|
||||||
|
"crates/compat/arje-localed-compat",
|
||||||
|
"crates/compat/arje-journald-compat",
|
||||||
|
"crates/compat/arje-resolved-compat",
|
||||||
|
"crates/compat/arje-polkit-compat",
|
||||||
|
"crates/compat/arje-machined-compat",
|
||||||
|
"crates/compat/arje-tmpfiles-compat",
|
||||||
|
"crates/compat/arje-systemd1-compat",
|
||||||
|
"crates/compat/arje-notify-compat",
|
||||||
|
"crates/compat/arje-binfmt-compat",
|
||||||
|
"crates/compat/arje-timer-compat",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/semantic_dht/ (minga) — DHT semántico de código
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/semantic_dht/minga-core",
|
"crates/modules/semantic_dht/minga-core",
|
||||||
"crates/modules/semantic_dht/minga-store",
|
"crates/modules/semantic_dht/minga-store",
|
||||||
@@ -48,132 +75,243 @@ members = [
|
|||||||
"crates/modules/semantic_dht/minga-cli",
|
"crates/modules/semantic_dht/minga-cli",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/ui_engine/ — Motor de widgets (yahweh libs+widgets)
|
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/ui_engine/libs/core",
|
"crates/modules/nahual/libs/core",
|
||||||
"crates/modules/ui_engine/libs/theme",
|
"crates/modules/nahual/libs/theme",
|
||||||
"crates/modules/ui_engine/libs/launcher",
|
"crates/modules/nahual/libs/launcher",
|
||||||
"crates/modules/ui_engine/libs/bus",
|
"crates/modules/nahual/libs/bus",
|
||||||
"crates/modules/ui_engine/libs/meta-schema",
|
"crates/modules/nahual/libs/meta-schema",
|
||||||
"crates/modules/ui_engine/libs/meta-runtime",
|
"crates/modules/nahual/libs/meta-runtime",
|
||||||
"crates/modules/ui_engine/libs/providers/fs",
|
"crates/modules/nahual/libs/providers/fs",
|
||||||
"crates/modules/ui_engine/libs/providers/sqlite",
|
"crates/modules/nahual/libs/providers/sqlite",
|
||||||
"crates/modules/ui_engine/widgets/tree",
|
"crates/modules/nahual/widgets/tree",
|
||||||
"crates/modules/ui_engine/widgets/container_core",
|
"crates/modules/nahual/widgets/container_core",
|
||||||
"crates/modules/ui_engine/widgets/splitter",
|
"crates/modules/nahual/widgets/splitter",
|
||||||
"crates/modules/ui_engine/widgets/tabs",
|
"crates/modules/nahual/widgets/tabs",
|
||||||
"crates/modules/ui_engine/widgets/tiled",
|
"crates/modules/nahual/widgets/tiled",
|
||||||
"crates/modules/ui_engine/widgets/text_input",
|
"crates/modules/nahual/widgets/text_input",
|
||||||
"crates/modules/ui_engine/widgets/meta-form",
|
"crates/modules/nahual/widgets/meta-form",
|
||||||
"crates/modules/ui_engine/widgets/banner",
|
"crates/modules/nahual/widgets/banner",
|
||||||
"crates/modules/ui_engine/widgets/card",
|
"crates/modules/nahual/widgets/card",
|
||||||
"crates/modules/ui_engine/widgets/stat-card",
|
"crates/modules/nahual/widgets/stat-card",
|
||||||
"crates/modules/ui_engine/widgets/app-header",
|
"crates/modules/nahual/widgets/app-header",
|
||||||
"crates/modules/ui_engine/widgets/theme-switcher",
|
"crates/modules/nahual/widgets/theme-switcher",
|
||||||
|
|
||||||
# --- lapaloma: módulo de gráficos data-viz (ver ARCHITECTURE.md fuente) ---
|
|
||||||
"crates/modules/ui_engine/libs/lapaloma-core",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-render",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-cartesian",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-stream",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-mesh",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-financial",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-polar",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-heatmap",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-treemap",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-flow",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-phosphor",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma-export",
|
|
||||||
"crates/modules/ui_engine/widgets/lapaloma",
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/nakui/ — ERP matemático (nakui absorbido)
|
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/pineal/core",
|
||||||
|
"crates/modules/pineal/render",
|
||||||
|
"crates/modules/pineal/cartesian",
|
||||||
|
"crates/modules/pineal/stream",
|
||||||
|
"crates/modules/pineal/mesh",
|
||||||
|
"crates/modules/pineal/financial",
|
||||||
|
"crates/modules/pineal/polar",
|
||||||
|
"crates/modules/pineal/heatmap",
|
||||||
|
"crates/modules/pineal/treemap",
|
||||||
|
"crates/modules/pineal/flow",
|
||||||
|
"crates/modules/pineal/phosphor",
|
||||||
|
"crates/modules/pineal/export",
|
||||||
|
"crates/modules/pineal/umbrella",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/verbo/ — Provider de embeddings model-agnostic
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/verbo/verbo-core",
|
||||||
|
"crates/modules/verbo/verbo-mock",
|
||||||
|
"crates/modules/verbo/verbo-daemon",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/agorapura/ — Identidad humana federada
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/agorapura/agorapura-core",
|
||||||
|
"crates/modules/agorapura/agorapura-graph",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/badu/ — Toma de notas con gravedad semántica
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/badu/badu-core",
|
||||||
|
"crates/modules/badu/badu-gravity",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/takiy/ — Composición musical asistida
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/takiy/takiy-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/matilda/ — Administración de servidores
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/matilda/matilda-core",
|
||||||
|
"crates/modules/matilda/matilda-config",
|
||||||
|
"crates/modules/matilda/matilda-plan",
|
||||||
|
"crates/modules/matilda/matilda-apply",
|
||||||
|
"crates/modules/matilda/matilda-ghost",
|
||||||
|
"crates/modules/matilda/matilda-linker",
|
||||||
|
"crates/modules/matilda/matilda-discover",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/yachay/ — Notebooks computacionales reproducibles
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/yachay/yachay-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/charka/ — Transpilador COBOL → Rust
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/charka/charka-bcd",
|
||||||
|
"crates/modules/charka/charka-lexer",
|
||||||
|
"crates/modules/charka/charka-parser",
|
||||||
|
"crates/modules/charka/charka-ir",
|
||||||
|
"crates/modules/charka/charka-runtime",
|
||||||
|
"crates/modules/charka/charka-codegen",
|
||||||
|
"crates/modules/charka/charka-shadow",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/mirada/ — Compositor Wayland
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/mirada/mirada-layout",
|
||||||
|
"crates/modules/mirada/mirada-protocol",
|
||||||
|
"crates/modules/mirada/mirada-brain",
|
||||||
|
"crates/modules/mirada/mirada-link",
|
||||||
|
"crates/modules/mirada/mirada-body",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/nakui/ — ERP matemático (categórico)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/nakui/core",
|
"crates/modules/nakui/core",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/nouser/ — explorador de Mónadas (nuevo)
|
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/nouser/card",
|
"crates/modules/chasqui/card",
|
||||||
"crates/modules/nouser/core",
|
"crates/modules/chasqui/core",
|
||||||
"crates/modules/nouser/nous",
|
"crates/modules/chasqui/nous",
|
||||||
"crates/modules/nouser/nous-mock",
|
"crates/modules/chasqui/nous-mock",
|
||||||
"crates/modules/nouser/nous-real",
|
"crates/modules/chasqui/nous-real",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/shipote/ — runtime de espacios aislados con flujo tipado
|
# modules/shuma/ — Runtime de espacios aislados (era shipote)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/shipote/shipote-card",
|
"crates/modules/shuma/shuma-card",
|
||||||
"crates/modules/shipote/shipote-protocol",
|
"crates/modules/shuma/shuma-protocol",
|
||||||
"crates/modules/shipote/shipote-discern",
|
"crates/modules/shuma/shuma-discern",
|
||||||
"crates/modules/shipote/shipote-core",
|
"crates/modules/shuma/shuma-core",
|
||||||
|
"crates/modules/shuma/shuma-intent",
|
||||||
|
"crates/modules/shuma/shuma-line",
|
||||||
|
"crates/modules/shuma/shuma-sysmon",
|
||||||
|
"crates/modules/shuma/shuma-session",
|
||||||
|
"crates/modules/shuma/shuma-exec",
|
||||||
|
"crates/modules/shuma/shuma-infer",
|
||||||
|
"crates/modules/shuma/shuma-shell-render",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/gioser/ — landing WASM (chacana + 4 elementos)
|
# modules/dominium/ — Simulador psicológico de campo medio
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/dominium/dominium-core",
|
||||||
|
"crates/modules/dominium/dominium-physics",
|
||||||
|
"crates/modules/dominium/dominium-iso",
|
||||||
|
"crates/modules/dominium/dominium-render-plan",
|
||||||
|
"crates/modules/dominium/dominium-canvas-gpui",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/gioser/gioser-geom",
|
"crates/modules/gioser/gioser-geom",
|
||||||
"crates/modules/gioser/gioser-physics",
|
"crates/modules/gioser/gioser-physics",
|
||||||
"crates/modules/gioser/gioser-palette",
|
"crates/modules/gioser/gioser-palette",
|
||||||
"crates/modules/gioser/gioser-shaders",
|
"crates/modules/gioser/gioser-shaders",
|
||||||
"crates/modules/gioser/gioser-canvas-web",
|
"crates/modules/gioser/gioser-canvas-web",
|
||||||
|
"crates/modules/gioser/gioser-graph-web",
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# modules/fana/ — Writer DAG editor (absorbe pluma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/fana/fana-core",
|
||||||
|
"crates/modules/fana/fana-graph",
|
||||||
|
"crates/modules/fana/fana-render-plan",
|
||||||
|
"crates/modules/fana/fana-editor-gpui",
|
||||||
|
"crates/modules/fana/fana-store",
|
||||||
|
"crates/modules/fana/fana-semantic",
|
||||||
|
"crates/modules/fana/fana-md",
|
||||||
|
"crates/modules/fana/fana-md-reader-web",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/pluma/ — markdown agnóstico + visor web elegante
|
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/pluma/pluma-md",
|
"crates/modules/revista/revista-core",
|
||||||
"crates/modules/pluma/pluma-reader-web",
|
"crates/modules/revista/revista-web",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/vista/ — deck horizontal swipe estilo Flutter PageView
|
# modules/barra/ — Taskbar agnóstica estilo Windows
|
||||||
# ============================================================
|
|
||||||
"crates/modules/vista/vista-web",
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# modules/barra/ — taskbar agnóstica estilo Windows
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
"crates/modules/barra/barra-core",
|
||||||
"crates/modules/barra/barra-web",
|
"crates/modules/barra/barra-web",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/cosmobiologia/ — estudio de astrología profesional
|
#### # modules/cosmobiologia/ — Estudio de astrología profesional
|
||||||
# ============================================================
|
#### # ============================================================
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-card",
|
#### "crates/modules/cosmobiologia/cosmobiologia-card",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-model",
|
#### "crates/modules/cosmobiologia/cosmobiologia-model",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-store",
|
#### "crates/modules/cosmobiologia/cosmobiologia-store",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-render",
|
#### "crates/modules/cosmobiologia/cosmobiologia-render",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-engine",
|
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-modules",
|
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-theme",
|
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-canvas",
|
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-tree",
|
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
|
||||||
"crates/modules/cosmobiologia/cosmobiologia-panel",
|
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-web",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# apps/ — apps que consumen el protocolo (yahweh modules+shell)
|
# apps/ — Binarios finales que consumen el protocolo
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/apps/file_explorer",
|
"crates/apps/brahman-broker-explorer",
|
||||||
"crates/apps/database_explorer",
|
"crates/apps/brahman-demo",
|
||||||
"crates/apps/text_viewer",
|
"crates/apps/sandokan",
|
||||||
"crates/apps/image_viewer",
|
"crates/apps/nahual-shell",
|
||||||
"crates/apps/yahweh-shell",
|
"crates/apps/nahual-file-explorer",
|
||||||
"crates/apps/nouser-explorer",
|
"crates/apps/nahual-database-explorer",
|
||||||
|
"crates/apps/nahual-text-viewer",
|
||||||
|
"crates/apps/nahual-image-viewer",
|
||||||
|
"crates/apps/chasqui-explorer",
|
||||||
"crates/apps/nakui-explorer",
|
"crates/apps/nakui-explorer",
|
||||||
"crates/apps/nakui-ui",
|
"crates/apps/nakui-ui",
|
||||||
"crates/apps/minga-explorer",
|
"crates/apps/minga-explorer",
|
||||||
"crates/apps/brahman-broker-explorer",
|
"crates/apps/shuma-daemon",
|
||||||
"crates/apps/brahman-demo",
|
"crates/apps/shuma-cli",
|
||||||
"crates/apps/shipote-daemon",
|
"crates/apps/shuma-gateway",
|
||||||
"crates/apps/shipote-cli",
|
"crates/apps/shuma-shell",
|
||||||
"crates/apps/shipote-gateway",
|
|
||||||
"crates/apps/shipote-shell",
|
|
||||||
"crates/apps/gioser-web",
|
"crates/apps/gioser-web",
|
||||||
"crates/apps/lapaloma-demo",
|
"crates/apps/pineal-demo",
|
||||||
"crates/apps/lapaloma-stream-demo",
|
"crates/apps/pineal-stream-demo",
|
||||||
"crates/apps/lapaloma-phosphor-demo",
|
"crates/apps/pineal-phosphor-demo",
|
||||||
"crates/apps/lapaloma-financial-demo",
|
"crates/apps/pineal-financial-demo",
|
||||||
"crates/apps/cosmobiologia",
|
#### "crates/apps/cosmobiologia",
|
||||||
"crates/apps/cosmobiologia-cli",
|
#### "crates/apps/cosmobiologia-cli",
|
||||||
|
#### "crates/apps/cosmobiologia-server",
|
||||||
|
"crates/apps/dominium",
|
||||||
|
"crates/apps/fana",
|
||||||
|
"crates/apps/agorapura",
|
||||||
|
"crates/apps/badu",
|
||||||
|
"crates/apps/matilda",
|
||||||
|
"crates/apps/yachay",
|
||||||
|
"crates/apps/mirada",
|
||||||
|
"crates/apps/mirada-compositor",
|
||||||
|
"crates/apps/mirada-ctl",
|
||||||
|
"crates/apps/mirada-launcher",
|
||||||
|
"crates/apps/mirada-portal",
|
||||||
|
"crates/apps/mirada-greeter",
|
||||||
|
"crates/apps/charka",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
|
||||||
|
# PROPIO workspace de Cargo: usa toolchain nightly, target
|
||||||
|
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
|
||||||
|
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
|
||||||
|
# crates compartidos se referencian por `path` cruzando la frontera.
|
||||||
|
exclude = ["renaser"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -190,6 +328,7 @@ serde_json = "1"
|
|||||||
serde-big-array = "0.5"
|
serde-big-array = "0.5"
|
||||||
postcard = { version = "1", features = ["use-std"] }
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
ron = "0.8"
|
||||||
bincode = "1"
|
bincode = "1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
@@ -222,7 +361,9 @@ argon2 = "0.5"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
||||||
# === WASM (arje) ===
|
# === WASM (arje) ===
|
||||||
wasmi = "0.40"
|
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
|
||||||
|
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
|
||||||
|
wasmi = "1.0"
|
||||||
wat = "1"
|
wat = "1"
|
||||||
|
|
||||||
# === Storage / DB ===
|
# === Storage / DB ===
|
||||||
@@ -234,6 +375,12 @@ libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macr
|
|||||||
libp2p-stream = "=0.4.0-alpha"
|
libp2p-stream = "=0.4.0-alpha"
|
||||||
libp2p-allow-block-list = "0.6"
|
libp2p-allow-block-list = "0.6"
|
||||||
|
|
||||||
|
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
|
||||||
|
russh = "0.54"
|
||||||
|
|
||||||
|
# === Math determinista cross-platform (dominium) ===
|
||||||
|
libm = "0.2"
|
||||||
|
|
||||||
# === Code parsing (minga) ===
|
# === Code parsing (minga) ===
|
||||||
tree-sitter = "0.24"
|
tree-sitter = "0.24"
|
||||||
tree-sitter-rust = "0.23"
|
tree-sitter-rust = "0.23"
|
||||||
@@ -245,17 +392,25 @@ tree-sitter-go = "0.23"
|
|||||||
# === FS notify ===
|
# === FS notify ===
|
||||||
notify = "6.1"
|
notify = "6.1"
|
||||||
|
|
||||||
|
# === FUSE (minga-vfs) ===
|
||||||
|
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
|
||||||
|
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
|
||||||
|
fuser = { version = "0.15", default-features = false }
|
||||||
|
|
||||||
# === CLI / auth (minga) ===
|
# === CLI / auth (minga) ===
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
rpassword = "7"
|
rpassword = "7"
|
||||||
|
|
||||||
|
# === PAM (brahman-auth) ===
|
||||||
|
pam = "0.8"
|
||||||
|
|
||||||
# === D-Bus (arje compat) ===
|
# === D-Bus (arje compat) ===
|
||||||
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||||
|
|
||||||
# === Tests ===
|
# === Tests ===
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
# === GPUI (yahweh) ===
|
# === GPUI (nahual) ===
|
||||||
gpui = "0.2"
|
gpui = "0.2"
|
||||||
|
|
||||||
# === Filesystem helpers ===
|
# === Filesystem helpers ===
|
||||||
@@ -272,40 +427,40 @@ glam = "0.30"
|
|||||||
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
|
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Intra-workspace deps de yahweh (referenciadas por workspace = true)
|
# Intra-workspace deps de nahual (referenciadas por workspace = true)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
yahweh-core = { path = "crates/modules/ui_engine/libs/core" }
|
nahual-core = { path = "crates/modules/nahual/libs/core" }
|
||||||
yahweh-theme = { path = "crates/modules/ui_engine/libs/theme" }
|
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
|
||||||
yahweh-bus = { path = "crates/modules/ui_engine/libs/bus" }
|
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
|
||||||
yahweh-provider-fs = { path = "crates/modules/ui_engine/libs/providers/fs" }
|
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
|
||||||
yahweh-provider-sqlite = { path = "crates/modules/ui_engine/libs/providers/sqlite" }
|
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
|
||||||
yahweh-widget-tree = { path = "crates/modules/ui_engine/widgets/tree" }
|
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
|
||||||
yahweh-widget-container-core = { path = "crates/modules/ui_engine/widgets/container_core" }
|
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
|
||||||
yahweh-widget-splitter = { path = "crates/modules/ui_engine/widgets/splitter" }
|
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
|
||||||
yahweh-widget-tabs = { path = "crates/modules/ui_engine/widgets/tabs" }
|
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
|
||||||
yahweh-widget-tiled = { path = "crates/modules/ui_engine/widgets/tiled" }
|
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
|
||||||
yahweh-widget-text-input = { path = "crates/modules/ui_engine/widgets/text_input" }
|
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
|
||||||
yahweh-file-explorer = { path = "crates/apps/file_explorer" }
|
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
|
||||||
yahweh-database-explorer = { path = "crates/apps/database_explorer" }
|
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
|
||||||
yahweh-text-viewer = { path = "crates/apps/text_viewer" }
|
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
|
||||||
yahweh-image-viewer = { path = "crates/apps/image_viewer" }
|
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Intra-workspace deps de lapaloma (módulo de gráficos)
|
# Intra-workspace deps de pineal (módulo de gráficos)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
lapaloma-core = { path = "crates/modules/ui_engine/libs/lapaloma-core" }
|
pineal-core = { path = "crates/modules/pineal/core" }
|
||||||
lapaloma-render = { path = "crates/modules/ui_engine/widgets/lapaloma-render" }
|
pineal-render = { path = "crates/modules/pineal/render" }
|
||||||
lapaloma-cartesian = { path = "crates/modules/ui_engine/widgets/lapaloma-cartesian" }
|
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
|
||||||
lapaloma-stream = { path = "crates/modules/ui_engine/widgets/lapaloma-stream" }
|
pineal-stream = { path = "crates/modules/pineal/stream" }
|
||||||
lapaloma-mesh = { path = "crates/modules/ui_engine/widgets/lapaloma-mesh" }
|
pineal-mesh = { path = "crates/modules/pineal/mesh" }
|
||||||
lapaloma-financial = { path = "crates/modules/ui_engine/widgets/lapaloma-financial" }
|
pineal-financial = { path = "crates/modules/pineal/financial" }
|
||||||
lapaloma-polar = { path = "crates/modules/ui_engine/widgets/lapaloma-polar" }
|
pineal-polar = { path = "crates/modules/pineal/polar" }
|
||||||
lapaloma-heatmap = { path = "crates/modules/ui_engine/widgets/lapaloma-heatmap" }
|
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
|
||||||
lapaloma-treemap = { path = "crates/modules/ui_engine/widgets/lapaloma-treemap" }
|
pineal-treemap = { path = "crates/modules/pineal/treemap" }
|
||||||
lapaloma-flow = { path = "crates/modules/ui_engine/widgets/lapaloma-flow" }
|
pineal-flow = { path = "crates/modules/pineal/flow" }
|
||||||
lapaloma-phosphor = { path = "crates/modules/ui_engine/widgets/lapaloma-phosphor" }
|
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
|
||||||
lapaloma-export = { path = "crates/modules/ui_engine/widgets/lapaloma-export" }
|
pineal-export = { path = "crates/modules/pineal/export" }
|
||||||
lapaloma = { path = "crates/modules/ui_engine/widgets/lapaloma" }
|
pineal = { path = "crates/modules/pineal/umbrella" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|||||||
+509
@@ -0,0 +1,509 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
# ============================================================
|
||||||
|
# protocol/ — Contratos canónicos + routing entre módulos
|
||||||
|
# ============================================================
|
||||||
|
"crates/protocol/brahman-card",
|
||||||
|
"crates/protocol/brahman-card-wit",
|
||||||
|
"crates/protocol/brahman-cards",
|
||||||
|
"crates/protocol/brahman-handshake",
|
||||||
|
"crates/protocol/brahman-broker",
|
||||||
|
"crates/protocol/brahman-admin",
|
||||||
|
"crates/protocol/brahman-sidecar",
|
||||||
|
"crates/protocol/brahman-net",
|
||||||
|
"crates/protocol/brahman-dht",
|
||||||
|
"crates/protocol/brahman-card-discovery",
|
||||||
|
"crates/protocol/brahman-ssh-multiplex",
|
||||||
|
"crates/protocol/brahman-auth",
|
||||||
|
"crates/protocol/arje-card",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# init/ — PID 1 + encarnación Linux (arje)
|
||||||
|
# ============================================================
|
||||||
|
"crates/init/arje-zero",
|
||||||
|
"crates/init/arje-kernel",
|
||||||
|
"crates/init/arje-soma",
|
||||||
|
"crates/init/arje-snapshot",
|
||||||
|
"crates/init/arje-incarnate",
|
||||||
|
"crates/init/arje-absorb",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
||||||
|
# ============================================================
|
||||||
|
"crates/runtime/arje-bus",
|
||||||
|
"crates/runtime/arje-cas",
|
||||||
|
"crates/runtime/arje-wasm",
|
||||||
|
"crates/runtime/arje-brain-rules",
|
||||||
|
"crates/runtime/arje-brain-cognitive",
|
||||||
|
"crates/runtime/arje-brain-audit",
|
||||||
|
"crates/runtime/arje-brain",
|
||||||
|
"crates/runtime/arje-echo",
|
||||||
|
"crates/runtime/sandokan-lifecycle",
|
||||||
|
"crates/runtime/sandokan-core",
|
||||||
|
"crates/runtime/sandokan-local",
|
||||||
|
"crates/runtime/sandokan-daemon",
|
||||||
|
"crates/runtime/sandokan-remote",
|
||||||
|
"crates/runtime/sandokan",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
|
# ============================================================
|
||||||
|
"crates/compat/arje-compat-common",
|
||||||
|
"crates/compat/arje-policy-provider",
|
||||||
|
"crates/compat/arje-logind-compat",
|
||||||
|
"crates/compat/arje-hostnamed-compat",
|
||||||
|
"crates/compat/arje-timedated-compat",
|
||||||
|
"crates/compat/arje-localed-compat",
|
||||||
|
"crates/compat/arje-journald-compat",
|
||||||
|
"crates/compat/arje-resolved-compat",
|
||||||
|
"crates/compat/arje-polkit-compat",
|
||||||
|
"crates/compat/arje-machined-compat",
|
||||||
|
"crates/compat/arje-tmpfiles-compat",
|
||||||
|
"crates/compat/arje-systemd1-compat",
|
||||||
|
"crates/compat/arje-notify-compat",
|
||||||
|
"crates/compat/arje-binfmt-compat",
|
||||||
|
"crates/compat/arje-timer-compat",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/semantic_dht/ (minga) — DHT semántico de código
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/semantic_dht/minga-core",
|
||||||
|
"crates/modules/semantic_dht/minga-store",
|
||||||
|
"crates/modules/semantic_dht/minga-p2p",
|
||||||
|
"crates/modules/semantic_dht/minga-vfs",
|
||||||
|
"crates/modules/semantic_dht/minga-cli",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/nahual/libs/core",
|
||||||
|
"crates/modules/nahual/libs/theme",
|
||||||
|
"crates/modules/nahual/libs/launcher",
|
||||||
|
"crates/modules/nahual/libs/bus",
|
||||||
|
"crates/modules/nahual/libs/meta-schema",
|
||||||
|
"crates/modules/nahual/libs/meta-runtime",
|
||||||
|
"crates/modules/nahual/libs/providers/fs",
|
||||||
|
"crates/modules/nahual/libs/providers/sqlite",
|
||||||
|
"crates/modules/nahual/widgets/tree",
|
||||||
|
"crates/modules/nahual/widgets/container_core",
|
||||||
|
"crates/modules/nahual/widgets/splitter",
|
||||||
|
"crates/modules/nahual/widgets/tabs",
|
||||||
|
"crates/modules/nahual/widgets/tiled",
|
||||||
|
"crates/modules/nahual/widgets/text_input",
|
||||||
|
"crates/modules/nahual/widgets/meta-form",
|
||||||
|
"crates/modules/nahual/widgets/banner",
|
||||||
|
"crates/modules/nahual/widgets/card",
|
||||||
|
"crates/modules/nahual/widgets/stat-card",
|
||||||
|
"crates/modules/nahual/widgets/app-header",
|
||||||
|
"crates/modules/nahual/widgets/theme-switcher",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/pineal/core",
|
||||||
|
"crates/modules/pineal/render",
|
||||||
|
"crates/modules/pineal/cartesian",
|
||||||
|
"crates/modules/pineal/stream",
|
||||||
|
"crates/modules/pineal/mesh",
|
||||||
|
"crates/modules/pineal/financial",
|
||||||
|
"crates/modules/pineal/polar",
|
||||||
|
"crates/modules/pineal/heatmap",
|
||||||
|
"crates/modules/pineal/treemap",
|
||||||
|
"crates/modules/pineal/flow",
|
||||||
|
"crates/modules/pineal/phosphor",
|
||||||
|
"crates/modules/pineal/export",
|
||||||
|
"crates/modules/pineal/umbrella",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/verbo/ — Provider de embeddings model-agnostic
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/verbo/verbo-core",
|
||||||
|
"crates/modules/verbo/verbo-mock",
|
||||||
|
"crates/modules/verbo/verbo-daemon",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/agorapura/ — Identidad humana federada
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/agorapura/agorapura-core",
|
||||||
|
"crates/modules/agorapura/agorapura-graph",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/badu/ — Toma de notas con gravedad semántica
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/badu/badu-core",
|
||||||
|
"crates/modules/badu/badu-gravity",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/takiy/ — Composición musical asistida
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/takiy/takiy-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/matilda/ — Administración de servidores
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/matilda/matilda-core",
|
||||||
|
"crates/modules/matilda/matilda-config",
|
||||||
|
"crates/modules/matilda/matilda-plan",
|
||||||
|
"crates/modules/matilda/matilda-apply",
|
||||||
|
"crates/modules/matilda/matilda-ghost",
|
||||||
|
"crates/modules/matilda/matilda-linker",
|
||||||
|
"crates/modules/matilda/matilda-discover",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/yachay/ — Notebooks computacionales reproducibles
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/yachay/yachay-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/charka/ — Transpilador COBOL → Rust
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/charka/charka-bcd",
|
||||||
|
"crates/modules/charka/charka-lexer",
|
||||||
|
"crates/modules/charka/charka-parser",
|
||||||
|
"crates/modules/charka/charka-ir",
|
||||||
|
"crates/modules/charka/charka-runtime",
|
||||||
|
"crates/modules/charka/charka-codegen",
|
||||||
|
"crates/modules/charka/charka-shadow",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/mirada/ — Compositor Wayland
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/mirada/mirada-layout",
|
||||||
|
"crates/modules/mirada/mirada-protocol",
|
||||||
|
"crates/modules/mirada/mirada-brain",
|
||||||
|
"crates/modules/mirada/mirada-link",
|
||||||
|
"crates/modules/mirada/mirada-body",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/nakui/ — ERP matemático (categórico)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/nakui/core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/chasqui/card",
|
||||||
|
"crates/modules/chasqui/core",
|
||||||
|
"crates/modules/chasqui/nous",
|
||||||
|
"crates/modules/chasqui/nous-mock",
|
||||||
|
"crates/modules/chasqui/nous-real",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/shuma/ — Runtime de espacios aislados (era shipote)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/shuma/shuma-card",
|
||||||
|
"crates/modules/shuma/shuma-protocol",
|
||||||
|
"crates/modules/shuma/shuma-discern",
|
||||||
|
"crates/modules/shuma/shuma-core",
|
||||||
|
"crates/modules/shuma/shuma-intent",
|
||||||
|
"crates/modules/shuma/shuma-line",
|
||||||
|
"crates/modules/shuma/shuma-sysmon",
|
||||||
|
"crates/modules/shuma/shuma-session",
|
||||||
|
"crates/modules/shuma/shuma-exec",
|
||||||
|
"crates/modules/shuma/shuma-infer",
|
||||||
|
"crates/modules/shuma/shuma-shell-render",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/dominium/ — Simulador psicológico de campo medio
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/dominium/dominium-core",
|
||||||
|
"crates/modules/dominium/dominium-physics",
|
||||||
|
"crates/modules/dominium/dominium-iso",
|
||||||
|
"crates/modules/dominium/dominium-render-plan",
|
||||||
|
"crates/modules/dominium/dominium-canvas-gpui",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/gioser/gioser-geom",
|
||||||
|
"crates/modules/gioser/gioser-physics",
|
||||||
|
"crates/modules/gioser/gioser-palette",
|
||||||
|
"crates/modules/gioser/gioser-shaders",
|
||||||
|
"crates/modules/gioser/gioser-canvas-web",
|
||||||
|
"crates/modules/gioser/gioser-graph-web",
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# modules/fana/ — Writer DAG editor (absorbe pluma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/fana/fana-core",
|
||||||
|
"crates/modules/fana/fana-graph",
|
||||||
|
"crates/modules/fana/fana-render-plan",
|
||||||
|
"crates/modules/fana/fana-editor-gpui",
|
||||||
|
"crates/modules/fana/fana-store",
|
||||||
|
"crates/modules/fana/fana-semantic",
|
||||||
|
"crates/modules/fana/fana-md",
|
||||||
|
"crates/modules/fana/fana-md-reader-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/revista/revista-core",
|
||||||
|
"crates/modules/revista/revista-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/barra/ — Taskbar agnóstica estilo Windows
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/barra/barra-core",
|
||||||
|
"crates/modules/barra/barra-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
#### # modules/cosmobiologia/ — Estudio de astrología profesional
|
||||||
|
#### # ============================================================
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-card",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-model",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-store",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-render",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# apps/ — Binarios finales que consumen el protocolo
|
||||||
|
# ============================================================
|
||||||
|
"crates/apps/brahman-broker-explorer",
|
||||||
|
"crates/apps/brahman-demo",
|
||||||
|
"crates/apps/sandokan",
|
||||||
|
"crates/apps/nahual-shell",
|
||||||
|
"crates/apps/nahual-file-explorer",
|
||||||
|
"crates/apps/nahual-database-explorer",
|
||||||
|
"crates/apps/nahual-text-viewer",
|
||||||
|
"crates/apps/nahual-image-viewer",
|
||||||
|
"crates/apps/chasqui-explorer",
|
||||||
|
"crates/apps/nakui-explorer",
|
||||||
|
"crates/apps/nakui-ui",
|
||||||
|
"crates/apps/minga-explorer",
|
||||||
|
"crates/apps/shuma-daemon",
|
||||||
|
"crates/apps/shuma-cli",
|
||||||
|
"crates/apps/shuma-gateway",
|
||||||
|
"crates/apps/shuma-shell",
|
||||||
|
"crates/apps/gioser-web",
|
||||||
|
"crates/apps/pineal-demo",
|
||||||
|
"crates/apps/pineal-stream-demo",
|
||||||
|
"crates/apps/pineal-phosphor-demo",
|
||||||
|
"crates/apps/pineal-financial-demo",
|
||||||
|
#### "crates/apps/cosmobiologia",
|
||||||
|
#### "crates/apps/cosmobiologia-cli",
|
||||||
|
#### "crates/apps/cosmobiologia-server",
|
||||||
|
"crates/apps/dominium",
|
||||||
|
"crates/apps/fana",
|
||||||
|
"crates/apps/agorapura",
|
||||||
|
"crates/apps/badu",
|
||||||
|
"crates/apps/matilda",
|
||||||
|
"crates/apps/yachay",
|
||||||
|
"crates/apps/mirada",
|
||||||
|
"crates/apps/mirada-compositor",
|
||||||
|
"crates/apps/mirada-ctl",
|
||||||
|
"crates/apps/mirada-launcher",
|
||||||
|
"crates/apps/mirada-portal",
|
||||||
|
"crates/apps/mirada-greeter",
|
||||||
|
"crates/apps/charka",
|
||||||
|
]
|
||||||
|
|
||||||
|
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
|
||||||
|
# PROPIO workspace de Cargo: usa toolchain nightly, target
|
||||||
|
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
|
||||||
|
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
|
||||||
|
# crates compartidos se referencian por `path` cruzando la frontera.
|
||||||
|
exclude = ["renaser"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
authors = ["Brahman Contributors"]
|
||||||
|
publish = false
|
||||||
|
repository = "https://example.invalid/brahman"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
# === Serialización ===
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde-big-array = "0.5"
|
||||||
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
|
toml = "0.8"
|
||||||
|
ron = "0.8"
|
||||||
|
bincode = "1"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# === Errores ===
|
||||||
|
thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
# === Async ===
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["compat"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# === Observabilidad ===
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
|
||||||
|
# === Linux primitives (arje) ===
|
||||||
|
nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
# === IDs / Hash / Crypto ===
|
||||||
|
ulid = { version = "1", features = ["serde"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
blake3 = "1.5"
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
argon2 = "0.5"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
# === WASM (arje) ===
|
||||||
|
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
|
||||||
|
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
|
||||||
|
wasmi = "1.0"
|
||||||
|
wat = "1"
|
||||||
|
|
||||||
|
# === Storage / DB ===
|
||||||
|
sled = "0.34"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled", "blob"] }
|
||||||
|
|
||||||
|
# === P2P (minga) ===
|
||||||
|
libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify"] }
|
||||||
|
libp2p-stream = "=0.4.0-alpha"
|
||||||
|
libp2p-allow-block-list = "0.6"
|
||||||
|
|
||||||
|
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
|
||||||
|
russh = "0.54"
|
||||||
|
|
||||||
|
# === Math determinista cross-platform (dominium) ===
|
||||||
|
libm = "0.2"
|
||||||
|
|
||||||
|
# === Code parsing (minga) ===
|
||||||
|
tree-sitter = "0.24"
|
||||||
|
tree-sitter-rust = "0.23"
|
||||||
|
tree-sitter-python = "0.23"
|
||||||
|
tree-sitter-typescript = "0.23"
|
||||||
|
tree-sitter-javascript = "0.23"
|
||||||
|
tree-sitter-go = "0.23"
|
||||||
|
|
||||||
|
# === FS notify ===
|
||||||
|
notify = "6.1"
|
||||||
|
|
||||||
|
# === FUSE (minga-vfs) ===
|
||||||
|
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
|
||||||
|
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
|
||||||
|
fuser = { version = "0.15", default-features = false }
|
||||||
|
|
||||||
|
# === CLI / auth (minga) ===
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
rpassword = "7"
|
||||||
|
|
||||||
|
# === PAM (brahman-auth) ===
|
||||||
|
pam = "0.8"
|
||||||
|
|
||||||
|
# === D-Bus (arje compat) ===
|
||||||
|
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||||
|
|
||||||
|
# === Tests ===
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
# === GPUI (nahual) ===
|
||||||
|
gpui = "0.2"
|
||||||
|
|
||||||
|
# === Filesystem helpers ===
|
||||||
|
directories = "5"
|
||||||
|
|
||||||
|
# === WASM web (gioser) ===
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = "0.3"
|
||||||
|
glam = "0.30"
|
||||||
|
|
||||||
|
# === Markdown (pluma) ===
|
||||||
|
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Intra-workspace deps de nahual (referenciadas por workspace = true)
|
||||||
|
# ============================================================
|
||||||
|
nahual-core = { path = "crates/modules/nahual/libs/core" }
|
||||||
|
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
|
||||||
|
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
|
||||||
|
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
|
||||||
|
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
|
||||||
|
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
|
||||||
|
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
|
||||||
|
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
|
||||||
|
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
|
||||||
|
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
|
||||||
|
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
|
||||||
|
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
|
||||||
|
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
|
||||||
|
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
|
||||||
|
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Intra-workspace deps de pineal (módulo de gráficos)
|
||||||
|
# ============================================================
|
||||||
|
pineal-core = { path = "crates/modules/pineal/core" }
|
||||||
|
pineal-render = { path = "crates/modules/pineal/render" }
|
||||||
|
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
|
||||||
|
pineal-stream = { path = "crates/modules/pineal/stream" }
|
||||||
|
pineal-mesh = { path = "crates/modules/pineal/mesh" }
|
||||||
|
pineal-financial = { path = "crates/modules/pineal/financial" }
|
||||||
|
pineal-polar = { path = "crates/modules/pineal/polar" }
|
||||||
|
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
|
||||||
|
pineal-treemap = { path = "crates/modules/pineal/treemap" }
|
||||||
|
pineal-flow = { path = "crates/modules/pineal/flow" }
|
||||||
|
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
|
||||||
|
pineal-export = { path = "crates/modules/pineal/export" }
|
||||||
|
pineal = { path = "crates/modules/pineal/umbrella" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "symbols"
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
# `line-tables-only` mantiene stack traces con archivo:línea correctos
|
||||||
|
# pero descarta el resto de symbols. Reduce target/ ~40% sin sacrificar
|
||||||
|
# debugging real para nuestro flujo (no usamos gdb sobre estos crates).
|
||||||
|
debug = "line-tables-only"
|
||||||
|
split-debuginfo = "unpacked"
|
||||||
|
incremental = true
|
||||||
|
# Más codegen-units = más paralelismo + builds incrementales más chicas
|
||||||
|
# (cada cambio re-compila menos). Default es 256 en dev pero lo
|
||||||
|
# anclamos para evitar regresiones.
|
||||||
|
codegen-units = 256
|
||||||
|
|
||||||
|
# Override puntual para deps grandes que NO debuggeamos: gpui, ort,
|
||||||
|
# fastembed, tokenizers, image. Subir opt-level acá hace que sus libs
|
||||||
|
# pesen menos en target/ (símbolos descartados durante la build).
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 0
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
|
[profile.dev.package.gpui]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.ort]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.fastembed]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.tokenizers]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.image]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
+509
@@ -0,0 +1,509 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
# ============================================================
|
||||||
|
# protocol/ — Contratos canónicos + routing entre módulos
|
||||||
|
# ============================================================
|
||||||
|
"crates/protocol/brahman-card",
|
||||||
|
"crates/protocol/brahman-card-wit",
|
||||||
|
"crates/protocol/brahman-cards",
|
||||||
|
"crates/protocol/brahman-handshake",
|
||||||
|
"crates/protocol/brahman-broker",
|
||||||
|
"crates/protocol/brahman-admin",
|
||||||
|
"crates/protocol/brahman-sidecar",
|
||||||
|
"crates/protocol/brahman-net",
|
||||||
|
"crates/protocol/brahman-dht",
|
||||||
|
"crates/protocol/brahman-card-discovery",
|
||||||
|
"crates/protocol/brahman-ssh-multiplex",
|
||||||
|
"crates/protocol/brahman-auth",
|
||||||
|
"crates/protocol/arje-card",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# init/ — PID 1 + encarnación Linux (arje)
|
||||||
|
# ============================================================
|
||||||
|
"crates/init/arje-zero",
|
||||||
|
"crates/init/arje-kernel",
|
||||||
|
"crates/init/arje-soma",
|
||||||
|
"crates/init/arje-snapshot",
|
||||||
|
"crates/init/arje-incarnate",
|
||||||
|
"crates/init/arje-absorb",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
||||||
|
# ============================================================
|
||||||
|
"crates/runtime/arje-bus",
|
||||||
|
"crates/runtime/arje-cas",
|
||||||
|
"crates/runtime/arje-wasm",
|
||||||
|
"crates/runtime/arje-brain-rules",
|
||||||
|
"crates/runtime/arje-brain-cognitive",
|
||||||
|
"crates/runtime/arje-brain-audit",
|
||||||
|
"crates/runtime/arje-brain",
|
||||||
|
"crates/runtime/arje-echo",
|
||||||
|
"crates/runtime/sandokan-lifecycle",
|
||||||
|
"crates/runtime/sandokan-core",
|
||||||
|
"crates/runtime/sandokan-local",
|
||||||
|
"crates/runtime/sandokan-daemon",
|
||||||
|
"crates/runtime/sandokan-remote",
|
||||||
|
"crates/runtime/sandokan",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# compat/ — Shims D-Bus para correr software systemd-aware
|
||||||
|
# ============================================================
|
||||||
|
"crates/compat/arje-compat-common",
|
||||||
|
"crates/compat/arje-policy-provider",
|
||||||
|
"crates/compat/arje-logind-compat",
|
||||||
|
"crates/compat/arje-hostnamed-compat",
|
||||||
|
"crates/compat/arje-timedated-compat",
|
||||||
|
"crates/compat/arje-localed-compat",
|
||||||
|
"crates/compat/arje-journald-compat",
|
||||||
|
"crates/compat/arje-resolved-compat",
|
||||||
|
"crates/compat/arje-polkit-compat",
|
||||||
|
"crates/compat/arje-machined-compat",
|
||||||
|
"crates/compat/arje-tmpfiles-compat",
|
||||||
|
"crates/compat/arje-systemd1-compat",
|
||||||
|
"crates/compat/arje-notify-compat",
|
||||||
|
"crates/compat/arje-binfmt-compat",
|
||||||
|
"crates/compat/arje-timer-compat",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/semantic_dht/ (minga) — DHT semántico de código
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/semantic_dht/minga-core",
|
||||||
|
"crates/modules/semantic_dht/minga-store",
|
||||||
|
"crates/modules/semantic_dht/minga-p2p",
|
||||||
|
"crates/modules/semantic_dht/minga-vfs",
|
||||||
|
"crates/modules/semantic_dht/minga-cli",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/nahual/ — Motor GPUI: libs + widgets (era yahweh)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/nahual/libs/core",
|
||||||
|
"crates/modules/nahual/libs/theme",
|
||||||
|
"crates/modules/nahual/libs/launcher",
|
||||||
|
"crates/modules/nahual/libs/bus",
|
||||||
|
"crates/modules/nahual/libs/meta-schema",
|
||||||
|
"crates/modules/nahual/libs/meta-runtime",
|
||||||
|
"crates/modules/nahual/libs/providers/fs",
|
||||||
|
"crates/modules/nahual/libs/providers/sqlite",
|
||||||
|
"crates/modules/nahual/widgets/tree",
|
||||||
|
"crates/modules/nahual/widgets/container_core",
|
||||||
|
"crates/modules/nahual/widgets/splitter",
|
||||||
|
"crates/modules/nahual/widgets/tabs",
|
||||||
|
"crates/modules/nahual/widgets/tiled",
|
||||||
|
"crates/modules/nahual/widgets/text_input",
|
||||||
|
"crates/modules/nahual/widgets/meta-form",
|
||||||
|
"crates/modules/nahual/widgets/banner",
|
||||||
|
"crates/modules/nahual/widgets/card",
|
||||||
|
"crates/modules/nahual/widgets/stat-card",
|
||||||
|
"crates/modules/nahual/widgets/app-header",
|
||||||
|
"crates/modules/nahual/widgets/theme-switcher",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/pineal/ — Data-viz agnóstica con backends (era lapaloma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/pineal/core",
|
||||||
|
"crates/modules/pineal/render",
|
||||||
|
"crates/modules/pineal/cartesian",
|
||||||
|
"crates/modules/pineal/stream",
|
||||||
|
"crates/modules/pineal/mesh",
|
||||||
|
"crates/modules/pineal/financial",
|
||||||
|
"crates/modules/pineal/polar",
|
||||||
|
"crates/modules/pineal/heatmap",
|
||||||
|
"crates/modules/pineal/treemap",
|
||||||
|
"crates/modules/pineal/flow",
|
||||||
|
"crates/modules/pineal/phosphor",
|
||||||
|
"crates/modules/pineal/export",
|
||||||
|
"crates/modules/pineal/umbrella",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/verbo/ — Provider de embeddings model-agnostic
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/verbo/verbo-core",
|
||||||
|
"crates/modules/verbo/verbo-mock",
|
||||||
|
"crates/modules/verbo/verbo-daemon",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/agorapura/ — Identidad humana federada
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/agorapura/agorapura-core",
|
||||||
|
"crates/modules/agorapura/agorapura-graph",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/badu/ — Toma de notas con gravedad semántica
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/badu/badu-core",
|
||||||
|
"crates/modules/badu/badu-gravity",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/takiy/ — Composición musical asistida
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/takiy/takiy-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/matilda/ — Administración de servidores
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/matilda/matilda-core",
|
||||||
|
"crates/modules/matilda/matilda-config",
|
||||||
|
"crates/modules/matilda/matilda-plan",
|
||||||
|
"crates/modules/matilda/matilda-apply",
|
||||||
|
"crates/modules/matilda/matilda-ghost",
|
||||||
|
"crates/modules/matilda/matilda-linker",
|
||||||
|
"crates/modules/matilda/matilda-discover",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/yachay/ — Notebooks computacionales reproducibles
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/yachay/yachay-core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/charka/ — Transpilador COBOL → Rust
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/charka/charka-bcd",
|
||||||
|
"crates/modules/charka/charka-lexer",
|
||||||
|
"crates/modules/charka/charka-parser",
|
||||||
|
"crates/modules/charka/charka-ir",
|
||||||
|
"crates/modules/charka/charka-runtime",
|
||||||
|
"crates/modules/charka/charka-codegen",
|
||||||
|
"crates/modules/charka/charka-shadow",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/mirada/ — Compositor Wayland
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/mirada/mirada-layout",
|
||||||
|
"crates/modules/mirada/mirada-protocol",
|
||||||
|
"crates/modules/mirada/mirada-brain",
|
||||||
|
"crates/modules/mirada/mirada-link",
|
||||||
|
"crates/modules/mirada/mirada-body",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/nakui/ — ERP matemático (categórico)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/nakui/core",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/chasqui/ — Explorador semántico de nómadas (ex-nouser, ex-akasha)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/chasqui/card",
|
||||||
|
"crates/modules/chasqui/core",
|
||||||
|
"crates/modules/chasqui/nous",
|
||||||
|
"crates/modules/chasqui/nous-mock",
|
||||||
|
"crates/modules/chasqui/nous-real",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/shuma/ — Runtime de espacios aislados (era shipote)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/shuma/shuma-card",
|
||||||
|
"crates/modules/shuma/shuma-protocol",
|
||||||
|
"crates/modules/shuma/shuma-discern",
|
||||||
|
"crates/modules/shuma/shuma-core",
|
||||||
|
"crates/modules/shuma/shuma-intent",
|
||||||
|
"crates/modules/shuma/shuma-line",
|
||||||
|
"crates/modules/shuma/shuma-sysmon",
|
||||||
|
"crates/modules/shuma/shuma-session",
|
||||||
|
"crates/modules/shuma/shuma-exec",
|
||||||
|
"crates/modules/shuma/shuma-infer",
|
||||||
|
"crates/modules/shuma/shuma-shell-render",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/dominium/ — Simulador psicológico de campo medio
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/dominium/dominium-core",
|
||||||
|
"crates/modules/dominium/dominium-physics",
|
||||||
|
"crates/modules/dominium/dominium-iso",
|
||||||
|
"crates/modules/dominium/dominium-render-plan",
|
||||||
|
"crates/modules/dominium/dominium-canvas-gpui",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/gioser/gioser-geom",
|
||||||
|
"crates/modules/gioser/gioser-physics",
|
||||||
|
"crates/modules/gioser/gioser-palette",
|
||||||
|
"crates/modules/gioser/gioser-shaders",
|
||||||
|
"crates/modules/gioser/gioser-canvas-web",
|
||||||
|
"crates/modules/gioser/gioser-graph-web",
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# modules/fana/ — Writer DAG editor (absorbe pluma)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/fana/fana-core",
|
||||||
|
"crates/modules/fana/fana-graph",
|
||||||
|
"crates/modules/fana/fana-render-plan",
|
||||||
|
"crates/modules/fana/fana-editor-gpui",
|
||||||
|
"crates/modules/fana/fana-store",
|
||||||
|
"crates/modules/fana/fana-semantic",
|
||||||
|
"crates/modules/fana/fana-md",
|
||||||
|
"crates/modules/fana/fana-md-reader-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/revista/ — Deck horizontal swipe (Flutter PageView)
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/revista/revista-core",
|
||||||
|
"crates/modules/revista/revista-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# modules/barra/ — Taskbar agnóstica estilo Windows
|
||||||
|
# ============================================================
|
||||||
|
"crates/modules/barra/barra-core",
|
||||||
|
"crates/modules/barra/barra-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
#### # modules/cosmobiologia/ — Estudio de astrología profesional
|
||||||
|
#### # ============================================================
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-card",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-model",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-store",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-render",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-corpus",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-engine",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-modules",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-theme",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-canvas",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-tree",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-panel",
|
||||||
|
#### "crates/modules/cosmobiologia/cosmobiologia-web",
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# apps/ — Binarios finales que consumen el protocolo
|
||||||
|
# ============================================================
|
||||||
|
"crates/apps/brahman-broker-explorer",
|
||||||
|
"crates/apps/brahman-demo",
|
||||||
|
"crates/apps/sandokan",
|
||||||
|
"crates/apps/nahual-shell",
|
||||||
|
"crates/apps/nahual-file-explorer",
|
||||||
|
"crates/apps/nahual-database-explorer",
|
||||||
|
"crates/apps/nahual-text-viewer",
|
||||||
|
"crates/apps/nahual-image-viewer",
|
||||||
|
"crates/apps/chasqui-explorer",
|
||||||
|
"crates/apps/nakui-explorer",
|
||||||
|
"crates/apps/nakui-ui",
|
||||||
|
"crates/apps/minga-explorer",
|
||||||
|
"crates/apps/shuma-daemon",
|
||||||
|
"crates/apps/shuma-cli",
|
||||||
|
"crates/apps/shuma-gateway",
|
||||||
|
"crates/apps/shuma-shell",
|
||||||
|
"crates/apps/gioser-web",
|
||||||
|
"crates/apps/pineal-demo",
|
||||||
|
"crates/apps/pineal-stream-demo",
|
||||||
|
"crates/apps/pineal-phosphor-demo",
|
||||||
|
"crates/apps/pineal-financial-demo",
|
||||||
|
#### "crates/apps/cosmobiologia",
|
||||||
|
#### "crates/apps/cosmobiologia-cli",
|
||||||
|
#### "crates/apps/cosmobiologia-server",
|
||||||
|
"crates/apps/dominium",
|
||||||
|
"crates/apps/fana",
|
||||||
|
"crates/apps/agorapura",
|
||||||
|
"crates/apps/badu",
|
||||||
|
"crates/apps/matilda",
|
||||||
|
"crates/apps/yachay",
|
||||||
|
"crates/apps/mirada",
|
||||||
|
"crates/apps/mirada-compositor",
|
||||||
|
"crates/apps/mirada-ctl",
|
||||||
|
"crates/apps/mirada-launcher",
|
||||||
|
"crates/apps/mirada-portal",
|
||||||
|
"crates/apps/mirada-greeter",
|
||||||
|
"crates/apps/charka",
|
||||||
|
]
|
||||||
|
|
||||||
|
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
|
||||||
|
# PROPIO workspace de Cargo: usa toolchain nightly, target
|
||||||
|
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
|
||||||
|
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
|
||||||
|
# crates compartidos se referencian por `path` cruzando la frontera.
|
||||||
|
exclude = ["renaser"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
authors = ["Brahman Contributors"]
|
||||||
|
publish = false
|
||||||
|
repository = "https://example.invalid/brahman"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
# === Serialización ===
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde-big-array = "0.5"
|
||||||
|
postcard = { version = "1", features = ["use-std"] }
|
||||||
|
toml = "0.8"
|
||||||
|
ron = "0.8"
|
||||||
|
bincode = "1"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# === Errores ===
|
||||||
|
thiserror = "2" # bump uniforme; arje (era 1) puede requerir ajustes menores
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
# === Async ===
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["compat"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# === Observabilidad ===
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
|
||||||
|
# === Linux primitives (arje) ===
|
||||||
|
nix = { version = "0.29", features = ["signal", "process", "sched", "mount", "fs", "socket", "net", "user"] }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
# === IDs / Hash / Crypto ===
|
||||||
|
ulid = { version = "1", features = ["serde"] }
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
sha2 = "0.10"
|
||||||
|
blake3 = "1.5"
|
||||||
|
ed25519-dalek = "2"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
argon2 = "0.5"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
# === WASM (arje) ===
|
||||||
|
# wasmi 1.0: unifica la versión con renaser (su kernel ya corre 1.0), para
|
||||||
|
# que el ABI WASM del host sea idéntico en Linux y en bare-metal.
|
||||||
|
wasmi = "1.0"
|
||||||
|
wat = "1"
|
||||||
|
|
||||||
|
# === Storage / DB ===
|
||||||
|
sled = "0.34"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled", "blob"] }
|
||||||
|
|
||||||
|
# === P2P (minga) ===
|
||||||
|
libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify"] }
|
||||||
|
libp2p-stream = "=0.4.0-alpha"
|
||||||
|
libp2p-allow-block-list = "0.6"
|
||||||
|
|
||||||
|
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
|
||||||
|
russh = "0.54"
|
||||||
|
|
||||||
|
# === Math determinista cross-platform (dominium) ===
|
||||||
|
libm = "0.2"
|
||||||
|
|
||||||
|
# === Code parsing (minga) ===
|
||||||
|
tree-sitter = "0.24"
|
||||||
|
tree-sitter-rust = "0.23"
|
||||||
|
tree-sitter-python = "0.23"
|
||||||
|
tree-sitter-typescript = "0.23"
|
||||||
|
tree-sitter-javascript = "0.23"
|
||||||
|
tree-sitter-go = "0.23"
|
||||||
|
|
||||||
|
# === FS notify ===
|
||||||
|
notify = "6.1"
|
||||||
|
|
||||||
|
# === FUSE (minga-vfs) ===
|
||||||
|
# default-features = false: prescinde de pkg-config/libfuse-dev en build.
|
||||||
|
# El montaje pasa a ser Rust puro (vía el helper `fusermount3` en runtime).
|
||||||
|
fuser = { version = "0.15", default-features = false }
|
||||||
|
|
||||||
|
# === CLI / auth (minga) ===
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
rpassword = "7"
|
||||||
|
|
||||||
|
# === PAM (brahman-auth) ===
|
||||||
|
pam = "0.8"
|
||||||
|
|
||||||
|
# === D-Bus (arje compat) ===
|
||||||
|
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||||
|
|
||||||
|
# === Tests ===
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
# === GPUI (nahual) ===
|
||||||
|
gpui = "0.2"
|
||||||
|
|
||||||
|
# === Filesystem helpers ===
|
||||||
|
directories = "5"
|
||||||
|
|
||||||
|
# === WASM web (gioser) ===
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = "0.3"
|
||||||
|
glam = "0.30"
|
||||||
|
|
||||||
|
# === Markdown (pluma) ===
|
||||||
|
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Intra-workspace deps de nahual (referenciadas por workspace = true)
|
||||||
|
# ============================================================
|
||||||
|
nahual-core = { path = "crates/modules/nahual/libs/core" }
|
||||||
|
nahual-theme = { path = "crates/modules/nahual/libs/theme" }
|
||||||
|
nahual-bus = { path = "crates/modules/nahual/libs/bus" }
|
||||||
|
nahual-provider-fs = { path = "crates/modules/nahual/libs/providers/fs" }
|
||||||
|
nahual-provider-sqlite = { path = "crates/modules/nahual/libs/providers/sqlite" }
|
||||||
|
nahual-widget-tree = { path = "crates/modules/nahual/widgets/tree" }
|
||||||
|
nahual-widget-container-core = { path = "crates/modules/nahual/widgets/container_core" }
|
||||||
|
nahual-widget-splitter = { path = "crates/modules/nahual/widgets/splitter" }
|
||||||
|
nahual-widget-tabs = { path = "crates/modules/nahual/widgets/tabs" }
|
||||||
|
nahual-widget-tiled = { path = "crates/modules/nahual/widgets/tiled" }
|
||||||
|
nahual-widget-text-input = { path = "crates/modules/nahual/widgets/text_input" }
|
||||||
|
nahual-file-explorer = { path = "crates/apps/nahual-file-explorer" }
|
||||||
|
nahual-database-explorer = { path = "crates/apps/nahual-database-explorer" }
|
||||||
|
nahual-text-viewer = { path = "crates/apps/nahual-text-viewer" }
|
||||||
|
nahual-image-viewer = { path = "crates/apps/nahual-image-viewer" }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Intra-workspace deps de pineal (módulo de gráficos)
|
||||||
|
# ============================================================
|
||||||
|
pineal-core = { path = "crates/modules/pineal/core" }
|
||||||
|
pineal-render = { path = "crates/modules/pineal/render" }
|
||||||
|
pineal-cartesian = { path = "crates/modules/pineal/cartesian" }
|
||||||
|
pineal-stream = { path = "crates/modules/pineal/stream" }
|
||||||
|
pineal-mesh = { path = "crates/modules/pineal/mesh" }
|
||||||
|
pineal-financial = { path = "crates/modules/pineal/financial" }
|
||||||
|
pineal-polar = { path = "crates/modules/pineal/polar" }
|
||||||
|
pineal-heatmap = { path = "crates/modules/pineal/heatmap" }
|
||||||
|
pineal-treemap = { path = "crates/modules/pineal/treemap" }
|
||||||
|
pineal-flow = { path = "crates/modules/pineal/flow" }
|
||||||
|
pineal-phosphor = { path = "crates/modules/pineal/phosphor" }
|
||||||
|
pineal-export = { path = "crates/modules/pineal/export" }
|
||||||
|
pineal = { path = "crates/modules/pineal/umbrella" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "symbols"
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
# `line-tables-only` mantiene stack traces con archivo:línea correctos
|
||||||
|
# pero descarta el resto de symbols. Reduce target/ ~40% sin sacrificar
|
||||||
|
# debugging real para nuestro flujo (no usamos gdb sobre estos crates).
|
||||||
|
debug = "line-tables-only"
|
||||||
|
split-debuginfo = "unpacked"
|
||||||
|
incremental = true
|
||||||
|
# Más codegen-units = más paralelismo + builds incrementales más chicas
|
||||||
|
# (cada cambio re-compila menos). Default es 256 en dev pero lo
|
||||||
|
# anclamos para evitar regresiones.
|
||||||
|
codegen-units = 256
|
||||||
|
|
||||||
|
# Override puntual para deps grandes que NO debuggeamos: gpui, ort,
|
||||||
|
# fastembed, tokenizers, image. Subir opt-level acá hace que sus libs
|
||||||
|
# pesen menos en target/ (símbolos descartados durante la build).
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 0
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
|
[profile.dev.package.gpui]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.ort]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.fastembed]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.tokenizers]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.dev.package.image]
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# apps/ — Binarios finales
|
||||||
|
|
||||||
|
**Propósito.** Aplicaciones ejecutables que consumen el protocolo
|
||||||
|
brahman y los módulos. Cada app es un `[[bin]]` o `cdylib` standalone.
|
||||||
|
|
||||||
|
## Mapa
|
||||||
|
|
||||||
|
### Protocol / Init
|
||||||
|
- `brahman-broker-explorer` — probe GPUI del broker (matches + sessions)
|
||||||
|
- `brahman-demo` — bootstrap reproducible: broker + producer + consumer
|
||||||
|
|
||||||
|
### Nahual (GPUI suite)
|
||||||
|
- `nahual-shell` — shell standard de explorers (sidebar+main+status)
|
||||||
|
- `nahual-file-explorer`, `nahual-database-explorer`,
|
||||||
|
`nahual-text-viewer`, `nahual-image-viewer`
|
||||||
|
|
||||||
|
### Akasha
|
||||||
|
- `akasha-explorer` — descubre el daemon `akasha-core` y lista Mónadas
|
||||||
|
|
||||||
|
### Nakui (ERP)
|
||||||
|
- `nakui-ui` — MetaUi+MetaForm con event log + replay
|
||||||
|
- `nakui-explorer` — dashboard sobre stack nahual
|
||||||
|
|
||||||
|
### Minga
|
||||||
|
- `minga-explorer` — dashboard de DHT semántico + indexer status
|
||||||
|
|
||||||
|
### Shuma (sandboxes)
|
||||||
|
- `shuma-daemon` — dueño de Workspaces (postcard sobre Unix socket)
|
||||||
|
- `shuma-cli` — CLI admin (`shipote` binario por compat)
|
||||||
|
- `shuma-gateway` — HTTP/JSON ↔ postcard
|
||||||
|
- `shuma-shell` — GUI GPUI de Workspaces
|
||||||
|
|
||||||
|
### Pineal (demos data-viz)
|
||||||
|
- `pineal-demo`, `pineal-stream-demo`, `pineal-phosphor-demo`,
|
||||||
|
`pineal-financial-demo`
|
||||||
|
|
||||||
|
### Web targets (cdylib WASM)
|
||||||
|
- `gioser-web` — landing chacana
|
||||||
|
- `cosmobiologia-web` (en modules/, no apps/) — bridge SVG
|
||||||
|
|
||||||
|
### Cosmobiología
|
||||||
|
- `cosmobiologia` — app GPUI principal (tree + canvas + panel)
|
||||||
|
- `cosmobiologia-cli` — calcula cartas headless
|
||||||
|
- `cosmobiologia-server` — server HTTP single-user con CRUD
|
||||||
|
|
||||||
|
## Convenciones
|
||||||
|
|
||||||
|
- Cada app declara su `Card` y se anuncia al Init vía `brahman-sidecar`.
|
||||||
|
- Apps que viven dentro de GPUI consumen `nahual-shell` para el chrome.
|
||||||
|
- Apps headless usan `clap` para argv.
|
||||||
|
- Tests E2E: usar `gpui::TestAppContext` para GPUI; CLI tests via
|
||||||
|
`tempfile` + `assert_cmd`.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "agorapura"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "agorapura — demostración narrada del ágora de identidad: identidades, claims, atestaciones firmadas y evaluación de confianza bajo políticas negociadas."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "agorapura"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
agorapura-core = { path = "../../modules/agorapura/agorapura-core" }
|
||||||
|
agorapura-graph = { path = "../../modules/agorapura/agorapura-graph" }
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! `agorapura` — demostración narrada del ágora de identidad.
|
||||||
|
//!
|
||||||
|
//! Recorre el escenario canónico de extremo a extremo: la institución
|
||||||
|
//! *Venezuela* atestigua la nacionalidad de la persona *Yumaira*, y
|
||||||
|
//! otras identidades la corroboran. Imprime la evidencia acumulada y
|
||||||
|
//! cómo distintas políticas *negociadas* la aceptan o no.
|
||||||
|
//!
|
||||||
|
//! No es la app definitiva — es un smoke test legible y la mejor forma
|
||||||
|
//! de ver el módulo funcionando: `cargo run -p agorapura`.
|
||||||
|
|
||||||
|
use agorapura_core::{Attestation, Claim, IdentityKind, Keypair};
|
||||||
|
use agorapura_graph::{TrustGraph, TrustPolicy};
|
||||||
|
|
||||||
|
/// Segundo Unix fijo para que la demo sea reproducible.
|
||||||
|
const T0: u64 = 1_700_000_000;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("\n ágora · demostración de identidad federada\n");
|
||||||
|
|
||||||
|
// --- Identidades. Semillas fijas → demo reproducible. ---
|
||||||
|
let yumaira = Keypair::from_seed([20; 32]);
|
||||||
|
let venezuela = Keypair::from_seed([10; 32]);
|
||||||
|
let comunidad = Keypair::from_seed([30; 32]);
|
||||||
|
let vecina = Keypair::from_seed([40; 32]);
|
||||||
|
|
||||||
|
let mut agora = TrustGraph::new();
|
||||||
|
agora.register(yumaira.identity(IdentityKind::Person, "Yumaira"));
|
||||||
|
agora.register(venezuela.identity(IdentityKind::Institution, "Venezuela"));
|
||||||
|
agora.register(comunidad.identity(IdentityKind::Community, "Vecinos del Valle"));
|
||||||
|
agora.register(vecina.identity(IdentityKind::Person, "Carmen"));
|
||||||
|
|
||||||
|
println!(" identidades registradas:");
|
||||||
|
for kp in [&yumaira, &venezuela, &comunidad, &vecina] {
|
||||||
|
let id = kp.identity_id();
|
||||||
|
let name = agora.identity(id).map(|i| i.display_name.as_str()).unwrap_or("?");
|
||||||
|
println!(" {id} {name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Atestaciones sobre la nacionalidad de Yumaira. ---
|
||||||
|
let nacionalidad = |by: &Keypair| {
|
||||||
|
Attestation::create(
|
||||||
|
by,
|
||||||
|
Claim::new(yumaira.identity_id(), "nacionalidad", "venezolana", T0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
println!("\n atestaciones de «nacionalidad = venezolana» sobre Yumaira:");
|
||||||
|
for (by, label) in [
|
||||||
|
(&venezuela, "Venezuela (institución)"),
|
||||||
|
(&comunidad, "Vecinos del Valle (comunidad)"),
|
||||||
|
(&yumaira, "Yumaira (ella misma)"),
|
||||||
|
] {
|
||||||
|
let att = nacionalidad(by);
|
||||||
|
match agora.add_attestation(att) {
|
||||||
|
Ok(()) => println!(" ✔ firma verificada — {label}"),
|
||||||
|
Err(e) => println!(" ✘ rechazada — {label}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Intento de fraude: una firma manipulada. ---
|
||||||
|
let mut falsa = nacionalidad(&vecina);
|
||||||
|
falsa.claim.value = "marciana".into(); // rompe la firma
|
||||||
|
print!("\n intento de atestación con firma manipulada: ");
|
||||||
|
match agora.add_attestation(falsa) {
|
||||||
|
Ok(()) => println!("ACEPTADA (esto sería un bug)"),
|
||||||
|
Err(e) => println!("rechazada — {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Corroboración. ---
|
||||||
|
let c = agora.corroboration(yumaira.identity_id(), "nacionalidad", "venezolana");
|
||||||
|
println!("\n corroboración del claim:");
|
||||||
|
println!(" atestadores totales : {}", c.total());
|
||||||
|
println!(" terceros (no ella) : {}", c.third_party());
|
||||||
|
println!(" auto-atestado : {}", c.self_attested);
|
||||||
|
|
||||||
|
// --- Veredicto según la política negociada. ---
|
||||||
|
println!("\n veredicto según la política (la verdad depende de lo pactado):");
|
||||||
|
for (policy, label) in [
|
||||||
|
(TrustPolicy::strict(1), "laxa · 1 tercero basta"),
|
||||||
|
(TrustPolicy::strict(2), "media · 2 terceros"),
|
||||||
|
(TrustPolicy::strict(3), "estricta · 3 terceros"),
|
||||||
|
] {
|
||||||
|
let ok = policy.accepts(&c);
|
||||||
|
let mark = if ok { "ACEPTA" } else { "rechaza" };
|
||||||
|
println!(" [{mark}] {label}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"\n el ágora no dicta la verdad: acumula evidencia firmada y\n \
|
||||||
|
cada quien la pesa con la política que negocie.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "badu"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "badu — demostración de la toma de notas: un cuaderno con wiki-links, backlinks, enlaces colgantes y clústeres por gravedad semántica."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "badu"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
badu-core = { path = "../../modules/badu/badu-core" }
|
||||||
|
badu-gravity = { path = "../../modules/badu/badu-gravity" }
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//! `badu` — demostración del cuaderno de notas.
|
||||||
|
//!
|
||||||
|
//! Siembra un cuaderno personal, imprime el grafo de wiki-links
|
||||||
|
//! (forward-links, backlinks, huérfanas, enlaces colgantes) y luego la
|
||||||
|
//! gravedad semántica: los clústeres por afinidad y los vecinos más
|
||||||
|
//! cercanos de una nota.
|
||||||
|
//!
|
||||||
|
//! Los vectores semánticos van a mano —tres tópicos: cocina, jardín,
|
||||||
|
//! oficina— para que el clustering se vea con claridad. En la app real
|
||||||
|
//! los produce `verbo`. Corre con `cargo run -p badu`.
|
||||||
|
|
||||||
|
use badu_core::{NoteId, NoteStore};
|
||||||
|
use badu_gravity::{GravityConfig, SemanticField};
|
||||||
|
|
||||||
|
/// Vector de tópico con un leve sesgo — notas del mismo tema quedan
|
||||||
|
/// afines sin ser idénticas.
|
||||||
|
fn topic(base: [f32; 3], nudge: f32) -> Vec<f32> {
|
||||||
|
vec![base[0] + nudge, base[1] + nudge * 0.3, base[2] - nudge * 0.2]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cocina = [1.0, 0.0, 0.0];
|
||||||
|
let jardin = [0.0, 1.0, 0.0];
|
||||||
|
let oficina = [0.0, 0.0, 1.0];
|
||||||
|
|
||||||
|
let mut store = NoteStore::new();
|
||||||
|
let mut field = SemanticField::new();
|
||||||
|
|
||||||
|
// (título, cuerpo, etiquetas, vector de tópico)
|
||||||
|
let seed: [(&str, &str, &[&str], Vec<f32>); 7] = [
|
||||||
|
(
|
||||||
|
"Índice",
|
||||||
|
"mi cuaderno: [[Recetas de la abuela]], [[Jardín]] y [[Oficina]]",
|
||||||
|
&["meta"],
|
||||||
|
topic(cocina, 0.0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Recetas de la abuela",
|
||||||
|
"sopa de auyama; ver también [[Lista del mercado]]",
|
||||||
|
&["cocina"],
|
||||||
|
topic(cocina, 0.05),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Lista del mercado",
|
||||||
|
"auyama, cilantro, pan; vuelve al [[Índice]]",
|
||||||
|
&["cocina"],
|
||||||
|
topic(cocina, 0.10),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Jardín",
|
||||||
|
"riego semanal; las [[Semillas de cilantro]] van en marzo",
|
||||||
|
&["jardín"],
|
||||||
|
topic(jardin, 0.05),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Semillas de cilantro",
|
||||||
|
"germinan en diez días",
|
||||||
|
&["jardín"],
|
||||||
|
topic(jardin, 0.10),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Oficina",
|
||||||
|
"[[Reunión del lunes]] y pendientes varios",
|
||||||
|
&["trabajo"],
|
||||||
|
topic(oficina, 0.05),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Diario sin enlaces",
|
||||||
|
"una nota suelta, no la enlaza nadie y enlaza a [[Algo Perdido]]",
|
||||||
|
&["personal"],
|
||||||
|
topic(oficina, 0.50),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut ids: Vec<(NoteId, String)> = Vec::new();
|
||||||
|
for (title, body, tags, vector) in seed {
|
||||||
|
let tags = tags.iter().map(|t| t.to_string()).collect();
|
||||||
|
let id = store.create(title, body, tags, 1_700_000_000);
|
||||||
|
field.insert(id, vector);
|
||||||
|
ids.push((id, title.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = |id: NoteId| {
|
||||||
|
ids.iter()
|
||||||
|
.find(|(i, _)| *i == id)
|
||||||
|
.map(|(_, n)| n.as_str())
|
||||||
|
.unwrap_or("?")
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("\n badu · cuaderno de notas — {} notas\n", store.len());
|
||||||
|
|
||||||
|
println!(" grafo de enlaces:");
|
||||||
|
for note in store.iter() {
|
||||||
|
let fwd: Vec<&str> = store.forward_links(note.id).into_iter().map(name).collect();
|
||||||
|
let back: Vec<&str> = store.backlinks(note.id).into_iter().map(name).collect();
|
||||||
|
println!(" «{}»", note.title);
|
||||||
|
println!(" enlaza a : {}", fmt_list(&fwd));
|
||||||
|
println!(" backlinks : {}", fmt_list(&back));
|
||||||
|
}
|
||||||
|
|
||||||
|
let orphans: Vec<&str> = store.orphans().iter().map(|n| n.title.as_str()).collect();
|
||||||
|
println!("\n notas huérfanas (sin backlinks): {}", fmt_list(&orphans));
|
||||||
|
let dangling_owned = store.dangling_links();
|
||||||
|
let dangling: Vec<&str> = dangling_owned.iter().map(|s| s.as_str()).collect();
|
||||||
|
println!(" enlaces colgantes (destino inexistente): {}", fmt_list(&dangling));
|
||||||
|
|
||||||
|
println!("\n gravedad semántica — clústeres (afinidad ≥ 0.85):");
|
||||||
|
for (n, cluster) in field.clusters(0.85).iter().enumerate() {
|
||||||
|
let titles: Vec<&str> = cluster.iter().map(|id| name(*id)).collect();
|
||||||
|
println!(" grupo {}: {}", n + 1, fmt_list(&titles));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pivot = ids[1].0; // "Recetas de la abuela"
|
||||||
|
println!("\n vecinos más afines a «{}»:", name(pivot));
|
||||||
|
for (id, score) in field.nearest(pivot, 3) {
|
||||||
|
println!(" {:.3} {}", score, name(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = field.gravity_layout(&GravityConfig::default());
|
||||||
|
println!("\n layout 2D por gravedad ({} posiciones):", layout.len());
|
||||||
|
for p in &layout {
|
||||||
|
println!(" ({:7.1}, {:7.1}) {}", p.x, p.y, name(p.id));
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formatea una lista de nombres, o `—` si está vacía.
|
||||||
|
fn fmt_list(items: &[&str]) -> String {
|
||||||
|
if items.is_empty() {
|
||||||
|
"—".to_string()
|
||||||
|
} else {
|
||||||
|
items.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,16 +6,16 @@ license.workspace = true
|
|||||||
description = "Probe GUI del broker brahman: conecta cada N segundos vía await_provider_blocking con un Card observer agnóstico, reporta 3 estados (down / up sin provider / up con provider)."
|
description = "Probe GUI del broker brahman: conecta cada N segundos vía await_provider_blocking con un Card observer agnóstico, reporta 3 estados (down / up sin provider / up con provider)."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
brahman-broker = { path = "../../core/brahman-broker" }
|
brahman-broker = { path = "../../protocol/brahman-broker" }
|
||||||
brahman-card = { path = "../../core/brahman-card" }
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
brahman-handshake = { path = "../../core/brahman-handshake" }
|
brahman-handshake = { path = "../../protocol/brahman-handshake" }
|
||||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
|
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
|
||||||
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
|
nahual-widget-stat-card = { path = "../../modules/nahual/widgets/stat-card" }
|
||||||
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
|
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ use ulid::Ulid;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
||||||
};
|
};
|
||||||
use yahweh_launcher::launch_app;
|
use nahual_launcher::launch_app;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_app_header::app_header;
|
use nahual_widget_app_header::app_header;
|
||||||
use yahweh_widget_banner::{banner_themed, Banner};
|
use nahual_widget_banner::{banner_themed, Banner};
|
||||||
use yahweh_widget_stat_card::stat_card;
|
use nahual_widget_stat_card::stat_card;
|
||||||
|
|
||||||
const POLL_INTERVAL: Duration = Duration::from_secs(5);
|
const POLL_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
|
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ license.workspace = true
|
|||||||
description = "Demo binaries de brahman: broker standalone + producer/consumer dummy. Pensados para que `scripts/bootstrap-demo.sh` arranque un escenario reproducible donde los 5 explorers ven sesiones, matches, y timeline."
|
description = "Demo binaries de brahman: broker standalone + producer/consumer dummy. Pensados para que `scripts/bootstrap-demo.sh` arranque un escenario reproducible donde los 5 explorers ven sesiones, matches, y timeline."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
brahman-broker = { path = "../../core/brahman-broker" }
|
brahman-broker = { path = "../../protocol/brahman-broker" }
|
||||||
brahman-card = { path = "../../core/brahman-card" }
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
brahman-handshake = { path = "../../core/brahman-handshake" }
|
brahman-handshake = { path = "../../protocol/brahman-handshake" }
|
||||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "charka"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "charka — CLI del transpilador COBOL→Rust: transpila, ejecuta y valida programas COBOL."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "charka"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
charka-lexer = { path = "../../modules/charka/charka-lexer" }
|
||||||
|
charka-parser = { path = "../../modules/charka/charka-parser" }
|
||||||
|
charka-ir = { path = "../../modules/charka/charka-ir" }
|
||||||
|
charka-codegen = { path = "../../modules/charka/charka-codegen" }
|
||||||
|
charka-shadow = { path = "../../modules/charka/charka-shadow" }
|
||||||
|
clap = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
//! `charka` — la CLI del transpilador COBOL → Rust.
|
||||||
|
//!
|
||||||
|
//! Envuelve el pipeline (lexer → parser → IR → codegen) y el validador
|
||||||
|
//! en sombra en cuatro comandos:
|
||||||
|
//!
|
||||||
|
//! - `transpile` — emite el código Rust de un fuente COBOL.
|
||||||
|
//! - `scaffold` — genera un crate Rust completo y compilable.
|
||||||
|
//! - `run` — ejecuta el programa (intérprete sombra) y lo imprime.
|
||||||
|
//! - `check` — ejecuta y compara la salida contra un archivo dado.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use charka_ir::{Ir, PerformTarget, Stmt};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
/// Ruta a `charka-runtime`, fijada al compilar — el crate generado por
|
||||||
|
/// `scaffold` la usa como dependencia.
|
||||||
|
const RUNTIME_PATH: &str = concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../modules/charka/charka-runtime"
|
||||||
|
);
|
||||||
|
|
||||||
|
/// El transpilador de COBOL a Rust.
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "charka", version, about = "Transpilador COBOL → Rust")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Transpila un fuente COBOL a código Rust.
|
||||||
|
Transpile {
|
||||||
|
/// El fuente COBOL (.cob), en formato libre.
|
||||||
|
input: PathBuf,
|
||||||
|
/// Archivo de salida; si se omite, va a la salida estándar.
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Genera un crate Rust completo y compilable.
|
||||||
|
Scaffold {
|
||||||
|
/// El fuente COBOL (.cob).
|
||||||
|
input: PathBuf,
|
||||||
|
/// El directorio del crate a crear.
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: PathBuf,
|
||||||
|
},
|
||||||
|
/// Ejecuta un programa COBOL (intérprete sombra) y muestra su salida.
|
||||||
|
Run {
|
||||||
|
/// El fuente COBOL (.cob).
|
||||||
|
input: PathBuf,
|
||||||
|
},
|
||||||
|
/// Ejecuta un programa y compara su salida con un archivo esperado.
|
||||||
|
Check {
|
||||||
|
/// El fuente COBOL (.cob).
|
||||||
|
input: PathBuf,
|
||||||
|
/// El archivo con la salida esperada.
|
||||||
|
#[arg(short, long)]
|
||||||
|
expect: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match dispatch(Cli::parse().command) {
|
||||||
|
Ok(code) => code,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("charka: {err:#}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(command: Command) -> Result<ExitCode> {
|
||||||
|
match command {
|
||||||
|
Command::Transpile { input, output } => transpile(&input, output.as_deref()),
|
||||||
|
Command::Scaffold { input, output } => scaffold(&input, &output),
|
||||||
|
Command::Run { input } => run(&input),
|
||||||
|
Command::Check { input, expect } => check(&input, &expect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Comandos ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn transpile(input: &Path, output: Option<&Path>) -> Result<ExitCode> {
|
||||||
|
let rust = charka_codegen::generate(&load_ir(input)?);
|
||||||
|
match output {
|
||||||
|
Some(path) => {
|
||||||
|
fs::write(path, rust)
|
||||||
|
.with_context(|| format!("no se pudo escribir {}", path.display()))?;
|
||||||
|
eprintln!("charka: escrito {}", path.display());
|
||||||
|
}
|
||||||
|
None => print!("{rust}"),
|
||||||
|
}
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scaffold(input: &Path, output: &Path) -> Result<ExitCode> {
|
||||||
|
let ir = load_ir(input)?;
|
||||||
|
let rust = charka_codegen::generate(&ir);
|
||||||
|
let name = crate_name(input);
|
||||||
|
|
||||||
|
fs::create_dir_all(output.join("src"))
|
||||||
|
.with_context(|| format!("no se pudo crear {}", output.display()))?;
|
||||||
|
fs::write(output.join("src/main.rs"), rust)?;
|
||||||
|
fs::write(output.join("Cargo.toml"), cargo_toml(&name))?;
|
||||||
|
|
||||||
|
eprintln!("charka: crate «{name}» generado en {}", output.display());
|
||||||
|
eprintln!(
|
||||||
|
" cargo run --manifest-path {}",
|
||||||
|
output.join("Cargo.toml").display()
|
||||||
|
);
|
||||||
|
warn_unknowns(&ir);
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(input: &Path) -> Result<ExitCode> {
|
||||||
|
let ir = load_ir(input)?;
|
||||||
|
let outcome = charka_shadow::interpret(&ir);
|
||||||
|
for line in &outcome.lines {
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
warn_unknowns(&ir);
|
||||||
|
if outcome.halt == charka_shadow::Halt::StepLimit {
|
||||||
|
eprintln!("charka: aviso — se agotó el tope de pasos (¿un bucle sin fin?)");
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(input: &Path, expect: &Path) -> Result<ExitCode> {
|
||||||
|
let ir = load_ir(input)?;
|
||||||
|
let outcome = charka_shadow::interpret(&ir);
|
||||||
|
let expected = fs::read_to_string(expect)
|
||||||
|
.with_context(|| format!("no se pudo leer {}", expect.display()))?;
|
||||||
|
|
||||||
|
let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect();
|
||||||
|
let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect();
|
||||||
|
|
||||||
|
if got == want {
|
||||||
|
println!("charka: OK — {} líneas coinciden", got.len());
|
||||||
|
Ok(ExitCode::SUCCESS)
|
||||||
|
} else {
|
||||||
|
eprintln!("charka: FALLA — la salida difiere de {}", expect.display());
|
||||||
|
report_diff(&got, &want);
|
||||||
|
Ok(ExitCode::FAILURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Apoyo ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Lee un fuente COBOL y lo lleva hasta el IR.
|
||||||
|
fn load_ir(input: &Path) -> Result<Ir> {
|
||||||
|
let source = fs::read_to_string(input)
|
||||||
|
.with_context(|| format!("no se pudo leer {}", input.display()))?;
|
||||||
|
let tokens =
|
||||||
|
charka_lexer::lex(&source, charka_lexer::SourceFormat::Free).context("error de léxico")?;
|
||||||
|
let program = charka_parser::parse(&tokens).context("error de parseo")?;
|
||||||
|
Ok(charka_ir::lower(&program))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El `Cargo.toml` de un crate generado por `scaffold`.
|
||||||
|
fn cargo_toml(name: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"[package]\n\
|
||||||
|
name = \"{name}\"\n\
|
||||||
|
version = \"0.1.0\"\n\
|
||||||
|
edition = \"2021\"\n\
|
||||||
|
\n\
|
||||||
|
[[bin]]\n\
|
||||||
|
name = \"{name}\"\n\
|
||||||
|
path = \"src/main.rs\"\n\
|
||||||
|
\n\
|
||||||
|
[dependencies]\n\
|
||||||
|
charka-runtime = {{ path = \"{RUNTIME_PATH}\" }}\n\
|
||||||
|
\n\
|
||||||
|
[workspace]\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un nombre de crate válido derivado del nombre del archivo fuente.
|
||||||
|
fn crate_name(input: &Path) -> String {
|
||||||
|
let stem = input
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("programa");
|
||||||
|
let mut name: String = stem
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c.is_ascii_alphanumeric() {
|
||||||
|
c.to_ascii_lowercase()
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||||
|
name = format!("cobol_{name}");
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Avisa de los verbos COBOL que el transpilador no soporta todavía.
|
||||||
|
fn warn_unknowns(ir: &Ir) {
|
||||||
|
let mut verbs = Vec::new();
|
||||||
|
for proc in &ir.procedures {
|
||||||
|
collect_unknowns(&proc.body, &mut verbs);
|
||||||
|
}
|
||||||
|
if verbs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verbs.sort();
|
||||||
|
verbs.dedup();
|
||||||
|
eprintln!(
|
||||||
|
"charka: aviso — verbos no transpilados (se omitieron): {}",
|
||||||
|
verbs.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recoge los verbos de los `Stmt::Unknown`, incluso los anidados.
|
||||||
|
fn collect_unknowns(stmts: &[Stmt], out: &mut Vec<String>) {
|
||||||
|
for s in stmts {
|
||||||
|
match s {
|
||||||
|
Stmt::Unknown { verb, .. } => out.push(verb.clone()),
|
||||||
|
Stmt::If {
|
||||||
|
then_branch,
|
||||||
|
else_branch,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
collect_unknowns(then_branch, out);
|
||||||
|
collect_unknowns(else_branch, out);
|
||||||
|
}
|
||||||
|
Stmt::Evaluate { whens, other, .. } => {
|
||||||
|
for w in whens {
|
||||||
|
collect_unknowns(&w.body, out);
|
||||||
|
}
|
||||||
|
collect_unknowns(other, out);
|
||||||
|
}
|
||||||
|
Stmt::Read {
|
||||||
|
at_end, not_at_end, ..
|
||||||
|
} => {
|
||||||
|
collect_unknowns(at_end, out);
|
||||||
|
collect_unknowns(not_at_end, out);
|
||||||
|
}
|
||||||
|
Stmt::Perform(p) => {
|
||||||
|
if let PerformTarget::Inline(body) = &p.target {
|
||||||
|
collect_unknowns(body, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imprime las líneas en que la salida obtenida difiere de la esperada.
|
||||||
|
fn report_diff(got: &[&str], want: &[&str]) {
|
||||||
|
for i in 0..got.len().max(want.len()) {
|
||||||
|
let g = got.get(i).copied().unwrap_or("<falta>");
|
||||||
|
let w = want.get(i).copied().unwrap_or("<falta>");
|
||||||
|
if g != w {
|
||||||
|
eprintln!(" línea {}:", i + 1);
|
||||||
|
eprintln!(" obtenido: {g}");
|
||||||
|
eprintln!(" esperado: {w}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn ir_of(src: &str) -> Ir {
|
||||||
|
let toks = charka_lexer::lex(src, charka_lexer::SourceFormat::Free).unwrap();
|
||||||
|
charka_ir::lower(&charka_parser::parse(&toks).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crate_name_is_sanitized() {
|
||||||
|
assert_eq!(crate_name(Path::new("/x/06-nomina.cob")), "cobol_06_nomina");
|
||||||
|
assert_eq!(crate_name(Path::new("PAYROLL.CBL")), "payroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cargo_toml_names_the_crate_and_the_runtime() {
|
||||||
|
let toml = cargo_toml("demo");
|
||||||
|
assert!(toml.contains("name = \"demo\""));
|
||||||
|
assert!(toml.contains("charka-runtime"));
|
||||||
|
assert!(toml.contains("[workspace]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_verbs_are_collected() {
|
||||||
|
let ir = ir_of(
|
||||||
|
"PROCEDURE DIVISION.\n\
|
||||||
|
MAIN.\n\
|
||||||
|
CALL 'SUBPROG'.\n",
|
||||||
|
);
|
||||||
|
let mut verbs = Vec::new();
|
||||||
|
for proc in &ir.procedures {
|
||||||
|
collect_unknowns(&proc.body, &mut verbs);
|
||||||
|
}
|
||||||
|
assert_eq!(verbs, vec!["CALL".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_program_has_no_unknowns() {
|
||||||
|
let ir = ir_of("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'OK'.\n STOP RUN.\n");
|
||||||
|
let mut verbs = Vec::new();
|
||||||
|
for proc in &ir.procedures {
|
||||||
|
collect_unknowns(&proc.body, &mut verbs);
|
||||||
|
}
|
||||||
|
assert!(verbs.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "chasqui-explorer"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Explorador GPUI de Mónadas: panel que descubre al daemon nouser vía broker brahman y consulta sus Mónadas dinámicamente."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
|
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
|
||||||
|
chasqui-card = { path = "../../modules/chasqui/card" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
|
||||||
|
nahual-widget-card = { path = "../../modules/nahual/widgets/card" }
|
||||||
|
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
|
||||||
|
gpui = { workspace = true }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "chasqui-explorer"
|
||||||
|
path = "src/main.rs"
|
||||||
+16
-16
@@ -1,21 +1,21 @@
|
|||||||
//! `nouser-explorer` — panel GPUI que descubre al daemon `nouser`
|
//! `chasqui-explorer` — panel GPUI que descubre al daemon `chasqui`
|
||||||
//! vía broker brahman y muestra sus Mónadas en vivo.
|
//! vía broker brahman y muestra sus Mónadas en vivo.
|
||||||
//!
|
//!
|
||||||
//! Diseño: ventana standalone que cada N segundos consulta el query
|
//! Diseño: ventana standalone que cada N segundos consulta el query
|
||||||
//! socket del daemon (`nouser_core::engine_socket::client::list_monads`).
|
//! socket del daemon (`chasqui_core::engine_socket::client::list_monads`).
|
||||||
//! El path del socket NO está hardcoded — se descubre vía
|
//! El path del socket NO está hardcoded — se descubre vía
|
||||||
//! `brahman_sidecar::await_provider_blocking` para el flow
|
//! `brahman_sidecar::await_provider_blocking` para el flow
|
||||||
//! `monad-list:json`. Si el daemon cae, el socket cacheado se invalida
|
//! `monad-list:json`. Si el daemon cae, el socket cacheado se invalida
|
||||||
//! y la próxima iteración re-descubre.
|
//! y la próxima iteración re-descubre.
|
||||||
//!
|
//!
|
||||||
//! Sin integración con yahweh-shell — es su propio binario para que el
|
//! Sin integración con nahual-shell — es su propio binario para que el
|
||||||
//! ecosistema sea visible incluso sin la shell completa.
|
//! ecosistema sea visible incluso sin la shell completa.
|
||||||
//!
|
//!
|
||||||
//! Uso:
|
//! Uso:
|
||||||
//! ```sh
|
//! ```sh
|
||||||
//! cargo run -p nouser-explorer
|
//! cargo run -p chasqui-explorer
|
||||||
//! # con override del init socket (heredado de brahman-handshake):
|
//! # con override del init socket (heredado de brahman-handshake):
|
||||||
//! BRAHMAN_INIT_SOCKET=/tmp/init.sock cargo run -p nouser-explorer
|
//! BRAHMAN_INIT_SOCKET=/tmp/init.sock cargo run -p chasqui-explorer
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -25,14 +25,14 @@ use brahman_sidecar::{await_provider_blocking, build_consumer_card, ConsumerErro
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window,
|
div, prelude::*, px, rgb, Context, IntoElement, Render, SharedString, Window,
|
||||||
};
|
};
|
||||||
use nouser_card::query::client as query_client;
|
use chasqui_card::query::client as query_client;
|
||||||
use nouser_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
|
use chasqui_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
|
||||||
use nouser_card::Lens;
|
use chasqui_card::Lens;
|
||||||
use yahweh_launcher::launch_app;
|
use nahual_launcher::launch_app;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_app_header::app_header;
|
use nahual_widget_app_header::app_header;
|
||||||
use yahweh_widget_banner::{banner_themed, Banner};
|
use nahual_widget_banner::{banner_themed, Banner};
|
||||||
use yahweh_widget_card::card_themed;
|
use nahual_widget_card::card_themed;
|
||||||
|
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
|
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
@@ -176,7 +176,7 @@ fn resolve_socket() -> Result<(PathBuf, &'static str), String> {
|
|||||||
/// Card con `flow.input = monad-list:json`, espera al primer
|
/// Card con `flow.input = monad-list:json`, espera al primer
|
||||||
/// `MatchEvent::Available`, devuelve el `producer_service_socket`.
|
/// `MatchEvent::Available`, devuelve el `producer_service_socket`.
|
||||||
fn discover_via_broker() -> Result<PathBuf, ConsumerError> {
|
fn discover_via_broker() -> Result<PathBuf, ConsumerError> {
|
||||||
let card = build_consumer_card("nouser-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
|
let card = build_consumer_card("chasqui-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
|
||||||
await_provider_blocking(card, DISCOVERY_TIMEOUT)
|
await_provider_blocking(card, DISCOVERY_TIMEOUT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ impl Render for Explorer {
|
|||||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
// Chrome viene del Theme global; los acentos por kind
|
// Chrome viene del Theme global; los acentos por kind
|
||||||
// (engine cyan, data purple) son señales semánticas del
|
// (engine cyan, data purple) son señales semánticas del
|
||||||
// dominio nouser y se mantienen locales.
|
// dominio chasqui y se mantienen locales.
|
||||||
let theme = Theme::global(cx).clone();
|
let theme = Theme::global(cx).clone();
|
||||||
let bg = theme.bg_app.clone();
|
let bg = theme.bg_app.clone();
|
||||||
let text = theme.fg_text;
|
let text = theme.fg_text;
|
||||||
@@ -205,7 +205,7 @@ impl Render for Explorer {
|
|||||||
.map(|w| format!(" · watching: {}", w))
|
.map(|w| format!(" · watching: {}", w))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
),
|
),
|
||||||
_ => "Buscando daemon nouser vía brahman-broker…".to_string(),
|
_ => "Buscando daemon chasqui vía brahman-broker…".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Header standard via widget compartido.
|
// Header standard via widget compartido.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "cosmobiologia-server"
|
||||||
|
version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
description = "Cosmobiología — server HTTP single-user. axum + cosmobiologia-engine. Sirve cartas y assets del cliente web. CRUD completo de groups/contacts/charts."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cosmobiologia-engine = { path = "../../modules/cosmobiologia/cosmobiologia-engine" }
|
||||||
|
cosmobiologia-model = { path = "../../modules/cosmobiologia/cosmobiologia-model" }
|
||||||
|
cosmobiologia-render = { path = "../../modules/cosmobiologia/cosmobiologia-render" }
|
||||||
|
cosmobiologia-store = { path = "../../modules/cosmobiologia/cosmobiologia-store" }
|
||||||
|
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
directories = { workspace = true }
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
axum = "0.7"
|
||||||
|
tower-http = { version = "0.6", features = ["cors", "trace", "fs"] }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cosmobiologia-server"
|
||||||
|
path = "src/main.rs"
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# Cosmobiología — guía de deploy
|
||||||
|
|
||||||
|
Server HTTP single-user, escrito en Rust + axum. Sirve cartas
|
||||||
|
astrológicas computadas con `cosmobiologia-engine` (VSOP2013 en Rust
|
||||||
|
puro) y la página web HTML/JS del cliente. Diseñado para correr
|
||||||
|
**local** o detrás de un reverse proxy con TLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Build
|
||||||
|
|
||||||
|
### Binario del server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release -p cosmobiologia-server
|
||||||
|
# ./target/release/cosmobiologia-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cliente WASM (opcional pero recomendado)
|
||||||
|
|
||||||
|
Sin esto, el cliente cae al **SSR**: cada interacción pide al server
|
||||||
|
el SVG recompuesto (~12 KB por click). Con WASM, el cliente compone
|
||||||
|
localmente — primera carga ~150 KB, después scrubbing instantáneo
|
||||||
|
sin round-trip.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Una sola vez:
|
||||||
|
cargo install wasm-pack
|
||||||
|
|
||||||
|
# Cada vez que cambie cosmobiologia-render o cosmobiologia-web:
|
||||||
|
cd crates/modules/cosmobiologia/cosmobiologia-web
|
||||||
|
wasm-pack build --release --target web \
|
||||||
|
--out-dir ../../../../apps/cosmobiologia-server/static/wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
`wasm-pack` produce `cosmobiologia_web.js` +
|
||||||
|
`cosmobiologia_web_bg.wasm` en
|
||||||
|
`crates/apps/cosmobiologia-server/static/wasm/`. El server los sirve
|
||||||
|
en `/static/wasm/*` y el `index.html` los importa con
|
||||||
|
`import init, { render_model_to_svg } from
|
||||||
|
'/static/wasm/cosmobiologia_web.js'`.
|
||||||
|
|
||||||
|
Si el directorio NO existe (build incompleto), el server devuelve
|
||||||
|
404 y el cliente cae al SSR automáticamente — sin error visible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Levantar el server
|
||||||
|
|
||||||
|
### Local (single-user, sin reverse proxy)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/cosmobiologia-server \
|
||||||
|
--port 8787 \
|
||||||
|
--bind 127.0.0.1 \
|
||||||
|
--db ~/.local/share/cosmobiologia/charts.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Abrí `http://127.0.0.1:8787/`. La DB es la misma que usa la app
|
||||||
|
desktop — cualquier carta creada en la app aparece en el browser
|
||||||
|
y viceversa.
|
||||||
|
|
||||||
|
### systemd (server público vía VPS)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/cosmobiologia.service
|
||||||
|
[Unit]
|
||||||
|
Description=Cosmobiología (server astrológico)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=cosmobio
|
||||||
|
Group=cosmobio
|
||||||
|
WorkingDirectory=/opt/cosmobiologia
|
||||||
|
ExecStart=/opt/cosmobiologia/cosmobiologia-server \
|
||||||
|
--port 8787 \
|
||||||
|
--bind 127.0.0.1 \
|
||||||
|
--db /var/lib/cosmobiologia/charts.db \
|
||||||
|
--static-wasm /opt/cosmobiologia/static/wasm
|
||||||
|
Environment=RUST_LOG=cosmobiologia_server=info,tower_http=warn
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
# Sandboxing básico
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/var/lib/cosmobiologia
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd -r -s /usr/sbin/nologin cosmobio
|
||||||
|
sudo mkdir -p /opt/cosmobiologia/static/wasm /var/lib/cosmobiologia
|
||||||
|
sudo cp target/release/cosmobiologia-server /opt/cosmobiologia/
|
||||||
|
sudo cp -r crates/apps/cosmobiologia-server/static/wasm/* \
|
||||||
|
/opt/cosmobiologia/static/wasm/
|
||||||
|
sudo chown -R cosmobio:cosmobio /opt/cosmobiologia /var/lib/cosmobiologia
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now cosmobiologia
|
||||||
|
sudo systemctl status cosmobiologia
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Reverse proxy (HTTPS + DNS bonito)
|
||||||
|
|
||||||
|
Con dos subdominios apuntando al host:
|
||||||
|
|
||||||
|
| DNS | Función |
|
||||||
|
|-----|---------|
|
||||||
|
| `cosmobiologia.gioser.net` | página web (HTML + WASM) |
|
||||||
|
| `api.cosmobiologia.gioser.net` | endpoints `/api/*` (JSON / SVG) |
|
||||||
|
|
||||||
|
Hoy el server sirve los dos roles en el mismo puerto — el split por
|
||||||
|
subdominio lo hace el proxy, **sin cambiar nada del Rust**.
|
||||||
|
|
||||||
|
### Caddyfile (recomendado — TLS automático con Let's Encrypt)
|
||||||
|
|
||||||
|
```Caddyfile
|
||||||
|
cosmobiologia.gioser.net {
|
||||||
|
encode gzip zstd
|
||||||
|
# Página web + estáticos + WASM
|
||||||
|
@api path /api/*
|
||||||
|
handle @api {
|
||||||
|
# Si el cliente pega un /api/ directo al subdominio principal,
|
||||||
|
# lo dejamos pasar (más amigable que 404).
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.cosmobiologia.gioser.net {
|
||||||
|
encode gzip zstd
|
||||||
|
# Solo los endpoints /api/*; rechaza el resto.
|
||||||
|
@api path /api/*
|
||||||
|
handle @api {
|
||||||
|
reverse_proxy 127.0.0.1:8787
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
respond "Use cosmobiologia.gioser.net para la página" 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### nginx (alternativa)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/cosmobiologia
|
||||||
|
server {
|
||||||
|
server_name cosmobiologia.gioser.net;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
# ssl_certificate / ssl_certificate_key — vía certbot
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/javascript application/wasm image/svg+xml application/json text/css text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name api.cosmobiologia.gioser.net;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
location / { return 404; }
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/json image/svg+xml;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS
|
||||||
|
|
||||||
|
A records (o AAAA si IPv6) hacia tu VPS:
|
||||||
|
|
||||||
|
```
|
||||||
|
cosmobiologia.gioser.net. A <ip-del-VPS>
|
||||||
|
api.cosmobiologia.gioser.net. A <ip-del-VPS>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. CORS y separación cliente↔API
|
||||||
|
|
||||||
|
Hoy el server tiene `CorsLayer::permissive()` — cualquier origen
|
||||||
|
puede hacer fetch contra `/api/*`. Eso es OK para:
|
||||||
|
|
||||||
|
- **Single-user local**: nadie más alcanza al server.
|
||||||
|
- **Demo público single-tenant**: misma DB para todos los visitantes,
|
||||||
|
sin datos sensibles. Los visitantes pueden leer y crear cartas
|
||||||
|
públicamente (es la naturaleza del demo).
|
||||||
|
|
||||||
|
**No use CorsLayer::permissive en producción multi-usuario**. Para
|
||||||
|
eso hay que:
|
||||||
|
1. Agregar auth (sesiones / JWT / API key).
|
||||||
|
2. Reemplazar con `CorsLayer::new().allow_origin(["https://cosmobiologia.gioser.net".parse().unwrap()])`.
|
||||||
|
3. Volverte el `AllowCredentials::yes()` si vas a usar cookies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Separación demo público ↔ desktop personal
|
||||||
|
|
||||||
|
El path por default de la DB (`~/.local/share/cosmobiologia/charts.db`)
|
||||||
|
es **compartido entre el server y la app desktop**. Eso es lo que
|
||||||
|
querés en tu máquina local — abrís el browser y ves las mismas
|
||||||
|
cartas que tenés en la app gpui.
|
||||||
|
|
||||||
|
**Pero NO querés que el server público en
|
||||||
|
`cosmobiologia.gioser.net` exponga TUS cartas privadas**. Para el
|
||||||
|
demo público:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# En tu VPS:
|
||||||
|
mkdir -p /var/lib/cosmobiologia
|
||||||
|
# Empezás con DB vacía (la app crea las tablas al primer arranque).
|
||||||
|
cosmobiologia-server --db /var/lib/cosmobiologia/charts.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Si querés precargar cartas demo (Einstein, una carta natal pública),
|
||||||
|
podés copiarlas desde tu DB local con la app, exportarlas como JSON
|
||||||
|
via `/api/charts/:id`, y postearlas al server público con POST
|
||||||
|
`/api/charts`. O simplemente abrir el browser, ir a "Nuevo
|
||||||
|
contacto" → "Nueva carta…" y cargarlas a mano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Backup
|
||||||
|
|
||||||
|
La DB SQLite es **un solo archivo**. Backup = `cp` (mientras el
|
||||||
|
server está parado, o usá `sqlite3 charts.db ".backup
|
||||||
|
charts.bak"` con el server corriendo).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Snapshot diario sin parar el server
|
||||||
|
sqlite3 /var/lib/cosmobiologia/charts.db ".backup /var/backups/cosmobiologia-$(date +%F).db"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Smoke test post-deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Desde tu máquina:
|
||||||
|
curl https://cosmobiologia.gioser.net/api/health
|
||||||
|
# → {"status":"ok","service":"cosmobiologia-server"}
|
||||||
|
|
||||||
|
curl https://cosmobiologia.gioser.net/api/sky | jq .title
|
||||||
|
# → "Cielo 2026-05-19 00:55 UTC"
|
||||||
|
|
||||||
|
# Abrí la página:
|
||||||
|
open https://cosmobiologia.gioser.net/
|
||||||
|
# (deberías ver la rueda del cielo + sidebar con "Cielo ahora")
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el cliente WASM cargó, en la barra inferior verás "WASM".
|
||||||
|
Si cayó al SSR, verás "SSR". Ambos modos son funcionales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Endpoints públicos (referencia)
|
||||||
|
|
||||||
|
| Método | Path | Función |
|
||||||
|
|--------|------|---------|
|
||||||
|
| GET | `/api/health` | healthcheck |
|
||||||
|
| GET | `/api/tree` | árbol completo (groups/contacts/charts) |
|
||||||
|
| GET | `/api/sky` | RenderModel "Cielo ahora" |
|
||||||
|
| GET | `/api/sky.svg` | SVG agnóstico del cielo (server-side) |
|
||||||
|
| GET | `/api/charts/:id` | Chart JSON |
|
||||||
|
| GET | `/api/charts/:id/render?...` | RenderModel con overlays |
|
||||||
|
| GET | `/api/charts/:id/svg?...` | SVG vía engine (svg_export) |
|
||||||
|
| GET | `/api/charts/:id/wheel.svg?...` | SVG vía render agnóstico |
|
||||||
|
| POST | `/api/charts` | crear carta |
|
||||||
|
| PATCH | `/api/charts/:id` | editar label/birth/config |
|
||||||
|
| DELETE | `/api/charts/:id` | borrar |
|
||||||
|
| POST | `/api/groups` | crear grupo |
|
||||||
|
| PATCH | `/api/groups/:id` | renombrar |
|
||||||
|
| DELETE | `/api/groups/:id` | borrar |
|
||||||
|
| POST | `/api/contacts` | crear contacto |
|
||||||
|
| PATCH | `/api/contacts/:id` | renombrar |
|
||||||
|
| DELETE | `/api/contacts/:id` | borrar |
|
||||||
|
|
||||||
|
Query params del render (`?...`):
|
||||||
|
|
||||||
|
- `offset_min=<i64>` — time scrubbing (minutos desde el natal).
|
||||||
|
- `transit=1` — activa overlay de tránsito al `now` del server.
|
||||||
|
- `prog_age=<f64>` — progresión secundaria a edad N.
|
||||||
|
- `sa_age=<f64>` — solar arc a edad N.
|
||||||
|
- `pd_age=<f64>` — primary directions GR (Naibod).
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
//! Cosmobiología — server HTTP single-user.
|
||||||
|
//!
|
||||||
|
//! - Reusa `cosmobiologia-engine` (VSOP2013 + LRU cache) nativo.
|
||||||
|
//! - Comparte (por default) la misma `charts.db` SQLite que la app
|
||||||
|
//! desktop, vía `directories::ProjectDirs::from("net", "gioser",
|
||||||
|
//! "cosmobiologia")`. La idea es: levantar `cosmobiologia-server`
|
||||||
|
//! en localhost y abrir el wheel desde el browser cuando no se está
|
||||||
|
//! con la app desktop.
|
||||||
|
//! - Single-user, sin auth, bind a `127.0.0.1` por default. NO debe
|
||||||
|
//! exponerse a la red pública sin agregar auth + HTTPS.
|
||||||
|
//!
|
||||||
|
//! ## Endpoints (v1)
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! GET /api/health healthcheck
|
||||||
|
//! GET /api/tree tree completo (groups + contacts + charts)
|
||||||
|
//! POST /api/groups crear grupo
|
||||||
|
//! PATCH /api/groups/:id renombrar
|
||||||
|
//! DELETE /api/groups/:id borrar
|
||||||
|
//! POST /api/contacts crear contacto
|
||||||
|
//! PATCH /api/contacts/:id renombrar
|
||||||
|
//! DELETE /api/contacts/:id borrar
|
||||||
|
//! POST /api/charts crear carta (contact_id + birth_data)
|
||||||
|
//! GET /api/charts/:id chart JSON
|
||||||
|
//! PATCH /api/charts/:id renombrar / editar birth_data
|
||||||
|
//! DELETE /api/charts/:id borrar
|
||||||
|
//! GET /api/charts/:id/render RenderModel JSON (overlays via query)
|
||||||
|
//! GET /api/charts/:id/svg SVG inline
|
||||||
|
//! GET /api/sky "Cielo ahora" — RenderModel UTC actual
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::extract::{Path, Query, State};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::routing::{get, patch, post};
|
||||||
|
use axum::{Json, Router};
|
||||||
|
use clap::Parser;
|
||||||
|
use cosmobiologia_engine::{
|
||||||
|
compose_with_options, svg_export, EngineError, NatalOptions, PipelineRequest, RenderModel,
|
||||||
|
};
|
||||||
|
use cosmobiologia_render::{compose_wheel, draw_commands_to_svg, CompositionOpts};
|
||||||
|
use cosmobiologia_model::{
|
||||||
|
Chart, ChartId, ChartKind, Contact, ContactId, Group, GroupId, StoredBirthData,
|
||||||
|
StoredChartConfig,
|
||||||
|
};
|
||||||
|
use cosmobiologia_store::Store;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "cosmobiologia-server",
|
||||||
|
about = "Servidor HTTP single-user de Cosmobiología."
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
/// Puerto donde escuchar. Default 8787.
|
||||||
|
#[arg(long, default_value = "8787")]
|
||||||
|
port: u16,
|
||||||
|
/// IP a bindear. Default `127.0.0.1` (solo localhost — single-user
|
||||||
|
/// sin auth).
|
||||||
|
#[arg(long, default_value = "127.0.0.1")]
|
||||||
|
bind: String,
|
||||||
|
/// Path al archivo SQLite. Default = el mismo de la app desktop
|
||||||
|
/// (`$XDG_DATA_HOME/cosmobiologia/charts.db`).
|
||||||
|
#[arg(long)]
|
||||||
|
db: Option<PathBuf>,
|
||||||
|
/// Directorio con los assets estáticos del cliente WASM
|
||||||
|
/// (output de `wasm-pack build --out-dir <este path>`). Si el
|
||||||
|
/// directorio no existe, el endpoint `/static/wasm/*` devuelve
|
||||||
|
/// 404 y el cliente cae al SSR.
|
||||||
|
#[arg(long, default_value = "crates/apps/cosmobiologia-server/static/wasm")]
|
||||||
|
static_wasm: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
store: Arc<Store>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| "cosmobiologia_server=info,tower_http=info".into()),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let db_path = match cli.db {
|
||||||
|
Some(p) => p,
|
||||||
|
None => default_db_path()?,
|
||||||
|
};
|
||||||
|
info!("DB: {}", db_path.display());
|
||||||
|
if let Some(parent) = db_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
let store = Arc::new(Store::open(&db_path)?);
|
||||||
|
|
||||||
|
let state = AppState { store };
|
||||||
|
let app = router()
|
||||||
|
.nest_service("/static/wasm", ServeDir::new(&cli.static_wasm))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let addr: SocketAddr = format!("{}:{}", cli.bind, cli.port).parse()?;
|
||||||
|
info!("listening on http://{}", addr);
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_db_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let dirs = directories::ProjectDirs::from("net", "gioser", "cosmobiologia")
|
||||||
|
.ok_or("no se pudo determinar XDG data dir")?;
|
||||||
|
Ok(dirs.data_dir().join("charts.db"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_index))
|
||||||
|
.route("/api/health", get(health))
|
||||||
|
.route("/api/tree", get(get_tree))
|
||||||
|
.route("/api/sky", get(get_sky))
|
||||||
|
// El render SVG agnóstico (via `cosmobiologia-render::compose_wheel`
|
||||||
|
// + `draw_commands_to_svg`) sirve a la fase 3 inicial: el
|
||||||
|
// cliente recibe SVG ya compuesto, sin necesidad de WASM.
|
||||||
|
// Cuando agreguemos el cliente WASM real, este endpoint se
|
||||||
|
// mantiene como fallback "ver SVG sin JS".
|
||||||
|
.route("/api/sky.svg", get(get_sky_svg))
|
||||||
|
.route("/api/charts/:id/wheel.svg", get(get_chart_wheel_svg))
|
||||||
|
.route("/api/groups", post(post_group))
|
||||||
|
.route("/api/groups/:id", patch(patch_group).delete(delete_group))
|
||||||
|
.route("/api/contacts", post(post_contact))
|
||||||
|
.route(
|
||||||
|
"/api/contacts/:id",
|
||||||
|
patch(patch_contact).delete(delete_contact),
|
||||||
|
)
|
||||||
|
.route("/api/charts", post(post_chart))
|
||||||
|
.route(
|
||||||
|
"/api/charts/:id",
|
||||||
|
get(get_chart).patch(patch_chart).delete(delete_chart),
|
||||||
|
)
|
||||||
|
.route("/api/charts/:id/render", get(get_chart_render))
|
||||||
|
.route("/api/charts/:id/svg", get(get_chart_svg))
|
||||||
|
.layer(CorsLayer::permissive()) // single-user, localhost: cors abierto
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Página HTML inicial
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
const INDEX_HTML: &str = include_str!("../static/index.html");
|
||||||
|
|
||||||
|
async fn get_index() -> Response {
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||||
|
INDEX_HTML.to_string(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG render agnóstico (no es el del engine — este viene de
|
||||||
|
// `cosmobiologia-render::compose_wheel` que es lo que mañana el
|
||||||
|
// cliente WASM también va a usar). Útil para demos sin WASM.
|
||||||
|
async fn get_sky_svg() -> Result<Response, ApiError> {
|
||||||
|
let chart = build_present_sky_chart();
|
||||||
|
let model = compose_with_options(&chart, 0, &[], &NatalOptions::default())?;
|
||||||
|
let cmds = compose_wheel(&model, &CompositionOpts::default());
|
||||||
|
let svg = draw_commands_to_svg(&cmds, 600.0);
|
||||||
|
Ok((
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
|
||||||
|
svg,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_chart_wheel_svg(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
Query(q): Query<RenderQuery>,
|
||||||
|
) -> Result<Response, ApiError> {
|
||||||
|
let chart = s
|
||||||
|
.store
|
||||||
|
.get_chart(id)
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||||
|
let model =
|
||||||
|
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
|
||||||
|
let cmds = compose_wheel(&model, &CompositionOpts::default());
|
||||||
|
let svg = draw_commands_to_svg(&cmds, 600.0);
|
||||||
|
Ok((
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
|
||||||
|
svg,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Error
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum ApiError {
|
||||||
|
#[error("not found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
#[error("bad request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
|
#[error("store: {0}")]
|
||||||
|
Store(#[from] cosmobiologia_store::StoreError),
|
||||||
|
#[error("engine: {0}")]
|
||||||
|
Engine(#[from] EngineError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (code, msg) = match &self {
|
||||||
|
ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
|
||||||
|
ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||||
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||||
|
};
|
||||||
|
(code, Json(serde_json::json!({ "error": msg }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiResult<T> = Result<Json<T>, ApiError>;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Health
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
async fn health() -> Json<serde_json::Value> {
|
||||||
|
Json(serde_json::json!({ "status": "ok", "service": "cosmobiologia-server" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Tree — listado completo
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct TreeNode {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
kind: &'static str, // "group" | "contact" | "chart"
|
||||||
|
children: Vec<TreeNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_tree(State(s): State<AppState>) -> ApiResult<Vec<TreeNode>> {
|
||||||
|
let mut roots = Vec::new();
|
||||||
|
// Grupos top-level
|
||||||
|
for g in s.store.list_groups(None)? {
|
||||||
|
roots.push(group_node(&s.store, &g)?);
|
||||||
|
}
|
||||||
|
// Contactos sin grupo (van bajo "General" en el tree desktop;
|
||||||
|
// acá los listamos directo al root para no confundir al cliente).
|
||||||
|
for c in s.store.list_contacts(None)? {
|
||||||
|
roots.push(contact_node(&s.store, &c)?);
|
||||||
|
}
|
||||||
|
Ok(Json(roots))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group_node(store: &Store, g: &Group) -> Result<TreeNode, ApiError> {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for sub in store.list_groups(Some(g.id))? {
|
||||||
|
children.push(group_node(store, &sub)?);
|
||||||
|
}
|
||||||
|
for c in store.list_contacts(Some(g.id))? {
|
||||||
|
children.push(contact_node(store, &c)?);
|
||||||
|
}
|
||||||
|
Ok(TreeNode {
|
||||||
|
id: format!("g:{}", g.id),
|
||||||
|
label: g.name.clone(),
|
||||||
|
kind: "group",
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contact_node(store: &Store, c: &Contact) -> Result<TreeNode, ApiError> {
|
||||||
|
let charts = store.list_charts(c.id).unwrap_or_default();
|
||||||
|
let children: Vec<TreeNode> = charts
|
||||||
|
.into_iter()
|
||||||
|
.map(|h| TreeNode {
|
||||||
|
id: format!("h:{}", h.id),
|
||||||
|
label: h.label,
|
||||||
|
kind: "chart",
|
||||||
|
children: Vec::new(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(TreeNode {
|
||||||
|
id: format!("c:{}", c.id),
|
||||||
|
label: c.name.clone(),
|
||||||
|
kind: "contact",
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Groups CRUD
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateGroupBody {
|
||||||
|
name: String,
|
||||||
|
parent: Option<GroupId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_group(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Json(b): Json<CreateGroupBody>,
|
||||||
|
) -> ApiResult<Group> {
|
||||||
|
let g = s.store.create_group(b.parent, &b.name, None)?;
|
||||||
|
Ok(Json(g))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PatchGroupBody {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_group(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<GroupId>,
|
||||||
|
Json(b): Json<PatchGroupBody>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
s.store.rename_group(id, &b.name)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_group(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<GroupId>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
s.store.delete_group(id)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Contacts CRUD
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateContactBody {
|
||||||
|
name: String,
|
||||||
|
group: Option<GroupId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_contact(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Json(b): Json<CreateContactBody>,
|
||||||
|
) -> ApiResult<Contact> {
|
||||||
|
let c = s.store.create_contact(b.group, &b.name, None)?;
|
||||||
|
Ok(Json(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PatchContactBody {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_contact(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ContactId>,
|
||||||
|
Json(b): Json<PatchContactBody>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
s.store.rename_contact(id, &b.name)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_contact(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ContactId>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
s.store.delete_contact(id)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Charts CRUD
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateChartBody {
|
||||||
|
contact_id: ContactId,
|
||||||
|
#[serde(default)]
|
||||||
|
kind: Option<ChartKind>,
|
||||||
|
label: String,
|
||||||
|
birth_data: StoredBirthData,
|
||||||
|
#[serde(default)]
|
||||||
|
config: Option<StoredChartConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_chart(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Json(b): Json<CreateChartBody>,
|
||||||
|
) -> ApiResult<Chart> {
|
||||||
|
let kind = b.kind.unwrap_or(ChartKind::Natal);
|
||||||
|
let cfg = b.config.unwrap_or_default();
|
||||||
|
let chart = s
|
||||||
|
.store
|
||||||
|
.create_chart(b.contact_id, kind, &b.label, &b.birth_data, &cfg, None)?;
|
||||||
|
Ok(Json(chart))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_chart(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
) -> ApiResult<Chart> {
|
||||||
|
let chart = s
|
||||||
|
.store
|
||||||
|
.get_chart(id)
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||||
|
Ok(Json(chart))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PatchChartBody {
|
||||||
|
#[serde(default)]
|
||||||
|
label: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
birth_data: Option<StoredBirthData>,
|
||||||
|
#[serde(default)]
|
||||||
|
config: Option<StoredChartConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_chart(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
Json(b): Json<PatchChartBody>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
let current = s
|
||||||
|
.store
|
||||||
|
.get_chart(id)
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||||
|
let label = b.label.unwrap_or(current.label);
|
||||||
|
let birth = b.birth_data.unwrap_or(current.birth_data);
|
||||||
|
let cfg = b.config.unwrap_or(current.config);
|
||||||
|
s.store.update_chart(id, &label, &birth, &cfg)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_chart(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
) -> ApiResult<serde_json::Value> {
|
||||||
|
s.store.delete_chart(id)?;
|
||||||
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Render
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
struct RenderQuery {
|
||||||
|
/// Offset de tiempo en minutos (para "scrubbing").
|
||||||
|
#[serde(default)]
|
||||||
|
offset_min: i64,
|
||||||
|
/// "1" = activar overlay de tránsitos al `now` del server.
|
||||||
|
#[serde(default)]
|
||||||
|
transit: u8,
|
||||||
|
/// Edad (años) — activa progresión secundaria si se setea.
|
||||||
|
#[serde(default)]
|
||||||
|
prog_age: Option<f64>,
|
||||||
|
/// Edad (años) — activa solar arc si se setea.
|
||||||
|
#[serde(default)]
|
||||||
|
sa_age: Option<f64>,
|
||||||
|
/// Edad (años) — activa primary directions si se setea.
|
||||||
|
#[serde(default)]
|
||||||
|
pd_age: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_requests(q: &RenderQuery) -> Vec<PipelineRequest> {
|
||||||
|
let mut r = Vec::new();
|
||||||
|
if q.transit == 1 {
|
||||||
|
r.push(PipelineRequest::Transit);
|
||||||
|
}
|
||||||
|
if let Some(a) = q.prog_age {
|
||||||
|
r.push(PipelineRequest::SecondaryProgression { target_age_years: a });
|
||||||
|
}
|
||||||
|
if let Some(a) = q.sa_age {
|
||||||
|
r.push(PipelineRequest::SolarArc { target_age_years: a });
|
||||||
|
}
|
||||||
|
if let Some(a) = q.pd_age {
|
||||||
|
r.push(PipelineRequest::PrimaryDirections {
|
||||||
|
target_age_years: a,
|
||||||
|
key: "naibod".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_chart_render(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
Query(q): Query<RenderQuery>,
|
||||||
|
) -> ApiResult<RenderModel> {
|
||||||
|
let chart = s
|
||||||
|
.store
|
||||||
|
.get_chart(id)
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||||
|
let model =
|
||||||
|
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
|
||||||
|
Ok(Json(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_chart_svg(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
Path(id): Path<ChartId>,
|
||||||
|
Query(q): Query<RenderQuery>,
|
||||||
|
) -> Result<Response, ApiError> {
|
||||||
|
let chart = s
|
||||||
|
.store
|
||||||
|
.get_chart(id)
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||||
|
let model =
|
||||||
|
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
|
||||||
|
let svg = svg_export::render_to_svg(&model);
|
||||||
|
Ok((
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
|
||||||
|
svg,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Sky now — sin chart
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
async fn get_sky() -> ApiResult<RenderModel> {
|
||||||
|
let chart = build_present_sky_chart();
|
||||||
|
let model = compose_with_options(&chart, 0, &[], &NatalOptions::default())?;
|
||||||
|
Ok(Json(model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_present_sky_chart() -> Chart {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let (year, month, day, hour, minute, second) = unix_to_civil_utc(secs);
|
||||||
|
let birth = StoredBirthData {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
second: second as f64,
|
||||||
|
tz_offset_minutes: 0,
|
||||||
|
latitude_deg: 51.4769, // Greenwich
|
||||||
|
longitude_deg: 0.0,
|
||||||
|
altitude_m: 47.0,
|
||||||
|
time_certainty: Default::default(),
|
||||||
|
subject_name: Some("Cielo".into()),
|
||||||
|
birthplace_label: Some("Greenwich (UTC)".into()),
|
||||||
|
};
|
||||||
|
Chart {
|
||||||
|
id: ChartId::default(),
|
||||||
|
contact_id: ContactId::default(),
|
||||||
|
kind: ChartKind::Natal,
|
||||||
|
label: format!(
|
||||||
|
"Cielo {:04}-{:02}-{:02} {:02}:{:02} UTC",
|
||||||
|
year, month, day, hour, minute
|
||||||
|
),
|
||||||
|
birth_data: birth,
|
||||||
|
config: StoredChartConfig::default(),
|
||||||
|
related_chart_id: None,
|
||||||
|
created_at_ms: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Howard Hinnant `days_to_civil` — Unix UTC → calendario.
|
||||||
|
/// Mismo algoritmo que en la app desktop; duplicado mínimo para no
|
||||||
|
/// arrastrar el shell entero como dep del server.
|
||||||
|
fn unix_to_civil_utc(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
|
||||||
|
let day_seconds: i64 = 86_400;
|
||||||
|
let z = secs.div_euclid(day_seconds);
|
||||||
|
let s = secs.rem_euclid(day_seconds);
|
||||||
|
let z = z + 719_468;
|
||||||
|
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
||||||
|
let doe = (z - era * 146_097) as u32;
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||||
|
let y = yoe as i64 + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let day = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let year = if month <= 2 { (y + 1) as i32 } else { y as i32 };
|
||||||
|
let hour = (s / 3600) as u32;
|
||||||
|
let minute = ((s % 3600) / 60) as u32;
|
||||||
|
let second = (s % 60) as u32;
|
||||||
|
(year, month, day, hour, minute, second)
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Cosmobiología</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1115;
|
||||||
|
--bg-panel: #171a21;
|
||||||
|
--fg: #e8e6df;
|
||||||
|
--fg-muted: #9aa0a8;
|
||||||
|
--accent: #c79a4d;
|
||||||
|
--border: #2a2e38;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: -apple-system, system-ui, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
aside {
|
||||||
|
width: 280px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 24px 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
.tree-node {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.tree-node:hover { background: rgba(255,255,255,.05); }
|
||||||
|
.tree-node.active { background: rgba(199,154,77,.15); color: var(--accent); }
|
||||||
|
.tree-node .icon { margin-right: 6px; opacity: .7; }
|
||||||
|
.indent-1 { padding-left: 22px; }
|
||||||
|
.indent-2 { padding-left: 38px; }
|
||||||
|
#wheel-container {
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#info {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
#info b { color: var(--fg); font-weight: 500; }
|
||||||
|
.toolbar {
|
||||||
|
width: 600px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.toolbar button {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--fg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toolbar button:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.toolbar label { font-size: 12px; color: var(--fg-muted); }
|
||||||
|
.toolbar input { width: 60px; padding: 4px; border: 1px solid var(--border);
|
||||||
|
background: var(--bg); color: var(--fg); border-radius: 4px; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside>
|
||||||
|
<h1>Cosmobiología</h1>
|
||||||
|
<div style="font-size:11px;color:var(--fg-muted);">cliente web demo</div>
|
||||||
|
|
||||||
|
<h2>Cartas</h2>
|
||||||
|
<div id="tree"></div>
|
||||||
|
|
||||||
|
<h2>Acciones rápidas</h2>
|
||||||
|
<div class="tree-node" onclick="loadSky()">⏱ Cielo ahora</div>
|
||||||
|
<div class="tree-node" onclick="newGroup()">+ Nuevo grupo</div>
|
||||||
|
<div class="tree-node" onclick="newContact()">+ Nuevo contacto</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="toolbar">
|
||||||
|
<button onclick="refreshSelected()">↻ Refrescar</button>
|
||||||
|
<label>Offset (min):
|
||||||
|
<input type="number" id="offset" value="0" step="60">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="transit"> Tránsito
|
||||||
|
</label>
|
||||||
|
<button onclick="downloadSvg()">⬇ SVG</button>
|
||||||
|
</div>
|
||||||
|
<div id="wheel-container">
|
||||||
|
<div style="color:var(--fg-muted)">Seleccioná una carta o "Cielo ahora"</div>
|
||||||
|
</div>
|
||||||
|
<div id="info"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let selectedChartId = null;
|
||||||
|
let mode = 'sky'; // 'sky' | 'chart'
|
||||||
|
|
||||||
|
async function loadTree() {
|
||||||
|
const tree = await fetch('/api/tree').then(r => r.json());
|
||||||
|
const el = document.getElementById('tree');
|
||||||
|
el.innerHTML = '';
|
||||||
|
renderNodes(tree, el, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodes(nodes, container, depth) {
|
||||||
|
for (const n of nodes) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `tree-node indent-${depth}`;
|
||||||
|
const icon = n.kind === 'group' ? '◇' : n.kind === 'contact' ? '◯' : '✦';
|
||||||
|
div.innerHTML = `<span class="icon">${icon}</span>${escapeHtml(n.label)}`;
|
||||||
|
if (n.kind === 'chart') {
|
||||||
|
const id = n.id.replace(/^h:/, '');
|
||||||
|
div.onclick = () => selectChart(id);
|
||||||
|
}
|
||||||
|
container.appendChild(div);
|
||||||
|
if (n.children && n.children.length) {
|
||||||
|
renderNodes(n.children, container, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectChart(id) {
|
||||||
|
selectedChartId = id;
|
||||||
|
mode = 'chart';
|
||||||
|
document.querySelectorAll('.tree-node').forEach(el =>
|
||||||
|
el.classList.toggle('active', el.textContent.endsWith(' (active)'))
|
||||||
|
);
|
||||||
|
await refreshSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSky() {
|
||||||
|
mode = 'sky';
|
||||||
|
selectedChartId = null;
|
||||||
|
await refreshSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cliente WASM opcional. Si `/static/wasm/cosmobiologia_web.js`
|
||||||
|
// existe (= el usuario corrió `wasm-pack build` después de
|
||||||
|
// `cargo build`), el rendering se hace localmente sin pedirle
|
||||||
|
// el SVG al server por cada interacción. Si NO existe (deploy
|
||||||
|
// mínimo), caemos al SSR de `/api/*.svg`. Toggle automático,
|
||||||
|
// sin configuración.
|
||||||
|
let wasm = null;
|
||||||
|
async function tryLoadWasm() {
|
||||||
|
try {
|
||||||
|
const mod = await import('/static/wasm/cosmobiologia_web.js');
|
||||||
|
await mod.default(); // wasm-pack: inicializa
|
||||||
|
wasm = mod;
|
||||||
|
document.getElementById('info').textContent = 'WASM cargado — render local';
|
||||||
|
} catch (e) {
|
||||||
|
console.info('WASM no disponible, usando SSR:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSelected() {
|
||||||
|
const offset = document.getElementById('offset').value || 0;
|
||||||
|
const transit = document.getElementById('transit').checked ? '1' : '0';
|
||||||
|
const params = new URLSearchParams({ offset_min: offset, transit });
|
||||||
|
const jsonUrl = mode === 'sky'
|
||||||
|
? `/api/sky`
|
||||||
|
: `/api/charts/${selectedChartId}/render?${params}`;
|
||||||
|
const ssrUrl = mode === 'sky'
|
||||||
|
? `/api/sky.svg`
|
||||||
|
: `/api/charts/${selectedChartId}/wheel.svg?${params}`;
|
||||||
|
|
||||||
|
const render = await fetch(jsonUrl).then(r => r.json()).catch(() => null);
|
||||||
|
let svg;
|
||||||
|
if (wasm && render) {
|
||||||
|
// Render local — WASM compose_wheel + draw_commands_to_svg.
|
||||||
|
try {
|
||||||
|
svg = wasm.render_model_to_svg(JSON.stringify(render), 600, 0);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('WASM render falló, fallback SSR:', e);
|
||||||
|
svg = await fetch(ssrUrl).then(r => r.text());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: SSR — el server devuelve el SVG ya compuesto.
|
||||||
|
svg = await fetch(ssrUrl).then(r => r.text());
|
||||||
|
}
|
||||||
|
document.getElementById('wheel-container').innerHTML = svg;
|
||||||
|
const info = document.getElementById('info');
|
||||||
|
if (render) {
|
||||||
|
const mode_label = wasm ? 'WASM' : 'SSR';
|
||||||
|
info.innerHTML =
|
||||||
|
`<b>${escapeHtml(render.title)}</b> · ` +
|
||||||
|
`Asc ${render.ascendant_deg.toFixed(2)}° · ` +
|
||||||
|
`MC ${render.midheaven_deg.toFixed(2)}° · ` +
|
||||||
|
`${render.compute_ms} ms · ${mode_label}`;
|
||||||
|
} else {
|
||||||
|
info.textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSvg() {
|
||||||
|
const offset = document.getElementById('offset').value || 0;
|
||||||
|
const transit = document.getElementById('transit').checked ? '1' : '0';
|
||||||
|
const params = new URLSearchParams({ offset_min: offset, transit });
|
||||||
|
const url = mode === 'sky'
|
||||||
|
? `/api/sky.svg`
|
||||||
|
: `/api/charts/${selectedChartId}/wheel.svg?${params}`;
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newGroup() {
|
||||||
|
const name = prompt('Nombre del grupo:');
|
||||||
|
if (!name) return;
|
||||||
|
await fetch('/api/groups', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
});
|
||||||
|
loadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newContact() {
|
||||||
|
const name = prompt('Nombre del contacto:');
|
||||||
|
if (!name) return;
|
||||||
|
await fetch('/api/contacts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
});
|
||||||
|
loadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return s.replace(/[&<>"']/g, c => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
}[c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await tryLoadWasm();
|
||||||
|
await loadTree();
|
||||||
|
await loadSky();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -15,13 +15,13 @@ cosmobiologia-panel = { path = "../../modules/cosmobiologia/cosmobiologia-panel"
|
|||||||
cosmobiologia-store = { path = "../../modules/cosmobiologia/cosmobiologia-store" }
|
cosmobiologia-store = { path = "../../modules/cosmobiologia/cosmobiologia-store" }
|
||||||
cosmobiologia-theme = { path = "../../modules/cosmobiologia/cosmobiologia-theme" }
|
cosmobiologia-theme = { path = "../../modules/cosmobiologia/cosmobiologia-theme" }
|
||||||
cosmobiologia-tree = { path = "../../modules/cosmobiologia/cosmobiologia-tree" }
|
cosmobiologia-tree = { path = "../../modules/cosmobiologia/cosmobiologia-tree" }
|
||||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
|
||||||
|
|
||||||
yahweh-core = { workspace = true }
|
nahual-core = { workspace = true }
|
||||||
yahweh-theme = { workspace = true }
|
nahual-theme = { workspace = true }
|
||||||
yahweh-widget-theme-switcher = { path = "../../modules/ui_engine/widgets/theme-switcher" }
|
nahual-widget-theme-switcher = { path = "../../modules/nahual/widgets/theme-switcher" }
|
||||||
yahweh-widget-splitter = { workspace = true }
|
nahual-widget-splitter = { workspace = true }
|
||||||
yahweh-widget-container-core = { workspace = true }
|
nahual-widget-container-core = { workspace = true }
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
directories = { workspace = true }
|
directories = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! (fire-and-forget; si no hay Init, la app sigue standalone).
|
//! (fire-and-forget; si no hay Init, la app sigue standalone).
|
||||||
//! 2. Abre la DB SQLite en `$XDG_DATA_HOME/cosmobiologia/charts.db`
|
//! 2. Abre la DB SQLite en `$XDG_DATA_HOME/cosmobiologia/charts.db`
|
||||||
//! (fallback a `~/.local/share/cosmobiologia/charts.db`).
|
//! (fallback a `~/.local/share/cosmobiologia/charts.db`).
|
||||||
//! 3. Levanta GPUI con [`yahweh_theme::Theme::install_default`].
|
//! 3. Levanta GPUI con [`nahual_theme::Theme::install_default`].
|
||||||
//! 4. Compone el shell: [`Shell`] dueño del tree (izq), canvas (centro)
|
//! 4. Compone el shell: [`Shell`] dueño del tree (izq), canvas (centro)
|
||||||
//! y panel (abajo). Cablea las suscripciones cross-widget.
|
//! y panel (abajo). Cablea las suscripciones cross-widget.
|
||||||
//!
|
//!
|
||||||
@@ -34,7 +34,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use cosmobiologia_store::Store;
|
use cosmobiologia_store::Store;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
use crate::shell::Shell;
|
use crate::shell::Shell;
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ use cosmobiologia_canvas::{
|
|||||||
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||||
};
|
};
|
||||||
use cosmobiologia_engine::{
|
use cosmobiologia_engine::{
|
||||||
LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options,
|
EventoConocido, LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest,
|
||||||
svg_export,
|
compose_with_options, svg_export,
|
||||||
};
|
};
|
||||||
use cosmobiologia_model::{
|
use cosmobiologia_model::{
|
||||||
Chart, ChartId, ChartKind, ContactId, FreeChartId, ModuleState, StoredBirthData,
|
Chart, ChartId, ChartKind, ContactId, FreeChartId, ModuleState, StoredBirthData,
|
||||||
@@ -44,11 +44,11 @@ use cosmobiologia_store::Store;
|
|||||||
use cosmobiologia_tree::{
|
use cosmobiologia_tree::{
|
||||||
parse_city_atlas_tsv, FreeChartEntry, TahuantinsuyuTree, TreeEvent,
|
parse_city_atlas_tsv, FreeChartEntry, TahuantinsuyuTree, TreeEvent,
|
||||||
};
|
};
|
||||||
use yahweh_core::{LayoutDirection, NodeId};
|
use nahual_core::{LayoutDirection, NodeId};
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_container_core::ChildSlot;
|
use nahual_widget_container_core::ChildSlot;
|
||||||
use yahweh_widget_splitter::{SplitContainer, SplitEvent};
|
use nahual_widget_splitter::{SplitContainer, SplitEvent};
|
||||||
use yahweh_widget_theme_switcher::theme_switcher;
|
use nahual_widget_theme_switcher::theme_switcher;
|
||||||
|
|
||||||
/// Posición del panel de control dentro del shell. `Bottom` mantiene
|
/// Posición del panel de control dentro del shell. `Bottom` mantiene
|
||||||
/// el layout histórico (tree+canvas arriba, panel abajo); las variantes
|
/// el layout histórico (tree+canvas arriba, panel abajo); las variantes
|
||||||
@@ -945,6 +945,7 @@ impl Shell {
|
|||||||
show_minors: read_bool("aspect_minors", false),
|
show_minors: read_bool("aspect_minors", false),
|
||||||
orb_multiplier: read_f64("orb_multiplier", 1.0),
|
orb_multiplier: read_f64("orb_multiplier", 1.0),
|
||||||
show_dignities: read_bool("show_dignities", false),
|
show_dignities: read_bool("show_dignities", false),
|
||||||
|
harmonic: read_f64("harmonic", 1.0).round().clamp(1.0, 64.0) as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,7 +1173,57 @@ impl Shell {
|
|||||||
CanvasEvent::ExportSvgRequested => {
|
CanvasEvent::ExportSvgRequested => {
|
||||||
self.export_current_to_svg();
|
self.export_current_to_svg();
|
||||||
}
|
}
|
||||||
|
CanvasEvent::GrAgeDelta(delta) => {
|
||||||
|
self.scrub_gr_age(*delta, cx);
|
||||||
}
|
}
|
||||||
|
CanvasEvent::HarmonicSelected(n) => {
|
||||||
|
self.select_harmonic(*n, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fija el armónico de la carta natal (clic en una barra del
|
||||||
|
/// espectro): escribe `harmonic` en `module_configs["natal"]`,
|
||||||
|
/// sincroniza el slider del panel y recompone.
|
||||||
|
fn select_harmonic(&mut self, n: u32, cx: &mut Context<Self>) {
|
||||||
|
let entry = self
|
||||||
|
.module_configs
|
||||||
|
.entry("natal".into())
|
||||||
|
.or_insert_with(|| serde_json::json!({}));
|
||||||
|
if let serde_json::Value::Object(map) = entry {
|
||||||
|
map.insert("harmonic".into(), serde_json::json!(n));
|
||||||
|
}
|
||||||
|
self.panel.update(cx, |p, cx| {
|
||||||
|
p.set_slider("natal", "harmonic", n as f64, cx)
|
||||||
|
});
|
||||||
|
self.persist_module("natal");
|
||||||
|
self.render_current(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubbing en vivo de la edad GR vía jog-dial. Acumula `delta`
|
||||||
|
/// sobre `target_age_years` del módulo `primary_directions`,
|
||||||
|
/// clampa a [0,120], sincroniza el slider del panel y recompone.
|
||||||
|
fn scrub_gr_age(&mut self, delta_years: f64, cx: &mut Context<Self>) {
|
||||||
|
if !module_enabled(&self.module_configs, "primary_directions") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.module_age_or_current("primary_directions");
|
||||||
|
let next = (current + delta_years).clamp(0.0, 120.0);
|
||||||
|
if (next - current).abs() < 1e-6 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = self
|
||||||
|
.module_configs
|
||||||
|
.entry("primary_directions".into())
|
||||||
|
.or_insert_with(|| serde_json::json!({}));
|
||||||
|
if let serde_json::Value::Object(map) = entry {
|
||||||
|
map.insert("target_age_years".into(), serde_json::json!(next));
|
||||||
|
}
|
||||||
|
self.panel.update(cx, |p, cx| {
|
||||||
|
p.set_slider("primary_directions", "target_age_years", next, cx)
|
||||||
|
});
|
||||||
|
self.persist_module("primary_directions");
|
||||||
|
self.render_current(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recompone la carta actual + escribe el SVG a un archivo en
|
/// Recompone la carta actual + escribe el SVG a un archivo en
|
||||||
@@ -1314,21 +1365,82 @@ impl Shell {
|
|||||||
/// Otros módulos overlay (progression, solar_arc, primary_directions)
|
/// Otros módulos overlay (progression, solar_arc, primary_directions)
|
||||||
/// son extensión natural — TODO.
|
/// son extensión natural — TODO.
|
||||||
fn on_panel_action(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
|
fn on_panel_action(&mut self, module_id: String, key: String, cx: &mut Context<Self>) {
|
||||||
if key != "save_as_free" {
|
match key.as_str() {
|
||||||
return;
|
"save_as_free" => match module_id.as_str() {
|
||||||
}
|
|
||||||
match module_id.as_str() {
|
|
||||||
"planetary_return" => self.save_planetary_return_as_free(cx),
|
"planetary_return" => self.save_planetary_return_as_free(cx),
|
||||||
"transit" => self.save_transit_as_free(cx),
|
"transit" => self.save_transit_as_free(cx),
|
||||||
"progression" => self.save_progression_as_free(cx),
|
"progression" => self.save_progression_as_free(cx),
|
||||||
// Solar arc y direcciones primarias son transformaciones
|
// Solar arc y direcciones primarias son transformaciones
|
||||||
// matemáticas puras (no tienen un birth_data real
|
// matemáticas puras (no tienen un birth_data real
|
||||||
// equivalente — un Chart natal computado en el "momento
|
// equivalente). Guardarlas exigiría un `ChartKind`
|
||||||
// SA" daría posiciones distintas a las dirigidas). Para
|
// `Derived { source, transform, params }`. TODO.
|
||||||
// guardarlas haría falta extender Chart con un kind
|
|
||||||
// `Derived { source, transform, params }` que el engine
|
|
||||||
// sepa rehidratar. TODO.
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
},
|
||||||
|
"rectificar" => self.run_rectificacion(cx),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lanza el rectificador automático (Sistema GR): lee las edades de
|
||||||
|
/// los eventos conocidos de los sliders del módulo, barre las horas
|
||||||
|
/// candidatas y escribe el resultado en el campo «Resultado» del
|
||||||
|
/// panel. El barrido es síncrono — para ±15 min son ~31 cartas.
|
||||||
|
fn run_rectificacion(&mut self, cx: &mut Context<Self>) {
|
||||||
|
// Clonamos la carta: `rectificar` necesita `&Chart` y luego
|
||||||
|
// `panel.update` toma `&mut self` — no pueden solaparse.
|
||||||
|
let Some(chart) = self.current_chart.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cfg = self.module_configs.get("primary_directions");
|
||||||
|
let read_age = |key: &str| -> f64 {
|
||||||
|
cfg.and_then(|c| c.get(key))
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
};
|
||||||
|
// Edades > 0 — una ranura en 0 es "sin usar".
|
||||||
|
let eventos: Vec<EventoConocido> = ["evento_1", "evento_2", "evento_3"]
|
||||||
|
.iter()
|
||||||
|
.map(|k| read_age(k))
|
||||||
|
.filter(|edad| *edad > 0.5)
|
||||||
|
.map(|edad| EventoConocido { edad_years: edad })
|
||||||
|
.collect();
|
||||||
|
let key_gr = cfg
|
||||||
|
.and_then(|c| c.get("key"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("naibod")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Ventana ±15 min — dos pasadas (minuto grueso, segundo fino).
|
||||||
|
match cosmobiologia_engine::rectificar(&chart, &eventos, 15, &key_gr) {
|
||||||
|
Ok(r) => {
|
||||||
|
// Offset en segundos → texto «±Xm Ys».
|
||||||
|
let seg = r.mejor_offset_segundos;
|
||||||
|
let signo = if seg < 0 { "-" } else { "+" };
|
||||||
|
let abs = seg.abs();
|
||||||
|
let resumen = format!(
|
||||||
|
"{signo}{}m {:02}s · error {:.2}a",
|
||||||
|
abs / 60,
|
||||||
|
abs % 60,
|
||||||
|
r.mejor_puntaje
|
||||||
|
);
|
||||||
|
self.panel.update(cx, |p, cx| {
|
||||||
|
p.set_string("primary_directions", "resultado", Some(resumen), cx)
|
||||||
|
});
|
||||||
|
// Publicar el perfil al canvas: dibuja la curva del
|
||||||
|
// barrido, cuyo valle marca la hora rectificada.
|
||||||
|
self.canvas
|
||||||
|
.update(cx, |c, cx| c.set_rectificacion(Some(r), cx));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.panel.update(cx, |p, cx| {
|
||||||
|
p.set_string(
|
||||||
|
"primary_directions",
|
||||||
|
"resultado",
|
||||||
|
Some("define al menos un evento (edad > 0)".to_string()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,7 +1783,7 @@ impl Shell {
|
|||||||
/// Click llama a `apply_dock` que reorganiza splitters y persiste.
|
/// Click llama a `apply_dock` que reorganiza splitters y persiste.
|
||||||
fn render_dock_switcher(
|
fn render_dock_switcher(
|
||||||
&self,
|
&self,
|
||||||
theme: &yahweh_theme::Theme,
|
theme: &nahual_theme::Theme,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let mut row = div()
|
let mut row = div()
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "dominium"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "dominium — simulador psicológico de campo medio: ventana GPUI con maqueta isométrica viva, panel de estadísticas y bucle de simulación."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "dominium"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dominium-core = { path = "../../modules/dominium/dominium-core" }
|
||||||
|
dominium-physics = { path = "../../modules/dominium/dominium-physics" }
|
||||||
|
dominium-iso = { path = "../../modules/dominium/dominium-iso" }
|
||||||
|
dominium-render-plan = { path = "../../modules/dominium/dominium-render-plan" }
|
||||||
|
dominium-canvas-gpui = { path = "../../modules/dominium/dominium-canvas-gpui" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
//! `dominium` — la ventana viva del simulador de campo medio.
|
||||||
|
//!
|
||||||
|
//! Compone toda la cadena de dominium en un app GPUI:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! dominium-core ─► dominium-physics ─► dominium-iso ─►
|
||||||
|
//! dominium-render-plan ─► dominium-canvas-gpui ─► [esta ventana]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Un bucle de fondo avanza la simulación ~11 veces por segundo; cada
|
||||||
|
//! tick reconstruye la maqueta isométrica y la repinta. El panel
|
||||||
|
//! derecho muestra las estadísticas agregadas y dos controles
|
||||||
|
//! (play/pausa, re-sembrar). Cuando la población colapsa, el mundo se
|
||||||
|
//! re-siembra solo: la demo nunca se queda en negro.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dominium_canvas_gpui::DominiumCanvas;
|
||||||
|
use dominium_core::{SimParams, World};
|
||||||
|
use dominium_iso::{IsoProjector, ZWeights};
|
||||||
|
use dominium_physics::tick;
|
||||||
|
use dominium_render_plan::{build_plan, PlanConfig};
|
||||||
|
use gpui::{
|
||||||
|
div, hsla, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use nahual_launcher::launch_app;
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
|
/// Lado de la grilla cuadrada del mundo.
|
||||||
|
const GRID: usize = 40;
|
||||||
|
/// Población inicial de Lemmings.
|
||||||
|
const LEMMINGS: usize = 50;
|
||||||
|
/// Periodo del bucle de simulación.
|
||||||
|
const TICK_MS: u64 = 90;
|
||||||
|
|
||||||
|
/// PRNG mínimo (LCG de 64 bits) — siembra reproducible sin dependencias.
|
||||||
|
struct Lcg(u64);
|
||||||
|
|
||||||
|
impl Lcg {
|
||||||
|
fn new(seed: u64) -> Self {
|
||||||
|
Self(seed)
|
||||||
|
}
|
||||||
|
fn next_u32(&mut self) -> u32 {
|
||||||
|
// Constantes de Knuth (MMIX).
|
||||||
|
self.0 = self
|
||||||
|
.0
|
||||||
|
.wrapping_mul(6364136223846793005)
|
||||||
|
.wrapping_add(1442695040888963407);
|
||||||
|
(self.0 >> 33) as u32
|
||||||
|
}
|
||||||
|
/// Flotante uniforme en `[0, 1)`.
|
||||||
|
fn next_f32(&mut self) -> f32 {
|
||||||
|
(self.next_u32() >> 8) as f32 / (1u32 << 24) as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Siembra un mundo: continentes de `materia`, vetas de `oro`, niebla de
|
||||||
|
/// `psique` y una población de Lemmings con sesgos y acciones variadas.
|
||||||
|
fn seed(seed: u64) -> World {
|
||||||
|
let mut w = World::new(GRID, GRID);
|
||||||
|
let mut rng = Lcg::new(seed);
|
||||||
|
for cy in 0..GRID {
|
||||||
|
for cx in 0..GRID {
|
||||||
|
let idx = w.grid.idx(cx, cy);
|
||||||
|
// m² concentra la materia en parches → aspecto de continentes.
|
||||||
|
let m = rng.next_f32();
|
||||||
|
w.grid.materia[idx] = m * m * 60.0;
|
||||||
|
if rng.next_f32() > 0.92 {
|
||||||
|
w.grid.oro[idx] = rng.next_f32() * 40.0;
|
||||||
|
}
|
||||||
|
w.grid.psique[idx] = rng.next_f32() * 12.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..LEMMINGS {
|
||||||
|
let x = rng.next_f32() * (GRID as f32 - 1.0);
|
||||||
|
let y = rng.next_f32() * (GRID as f32 - 1.0);
|
||||||
|
let psi = [
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
rng.next_f32(),
|
||||||
|
];
|
||||||
|
let i = w.lemmings.spawn(x, y, 30.0 + rng.next_f32() * 40.0, psi);
|
||||||
|
w.lemmings.accion[i] = (rng.next_u32() % 6) as u8;
|
||||||
|
}
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estadísticas agregadas de un instante de la simulación.
|
||||||
|
struct Stats {
|
||||||
|
poblacion: usize,
|
||||||
|
materia: f32,
|
||||||
|
oro: f32,
|
||||||
|
energia: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El estado del simulador y su presentación.
|
||||||
|
struct Sim {
|
||||||
|
world: World,
|
||||||
|
params: SimParams,
|
||||||
|
iso: IsoProjector,
|
||||||
|
weights: ZWeights,
|
||||||
|
cfg: PlanConfig,
|
||||||
|
running: bool,
|
||||||
|
/// Ticks transcurridos en la época actual.
|
||||||
|
tick: u64,
|
||||||
|
/// Cuántas veces se re-sembró el mundo (colapso poblacional).
|
||||||
|
epoch: u64,
|
||||||
|
/// Semilla rodante para cada re-siembra.
|
||||||
|
rng_seed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sim {
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let rng_seed = 0xD0_31_31_07;
|
||||||
|
let sim = Self {
|
||||||
|
world: seed(rng_seed),
|
||||||
|
params: SimParams::default(),
|
||||||
|
iso: IsoProjector::new(12.0, 0.05),
|
||||||
|
weights: ZWeights::default(),
|
||||||
|
cfg: PlanConfig {
|
||||||
|
tile: 15.0,
|
||||||
|
lemming_size: 8.0,
|
||||||
|
lemming_lift: 0.7,
|
||||||
|
palette: Default::default(),
|
||||||
|
},
|
||||||
|
running: true,
|
||||||
|
tick: 0,
|
||||||
|
epoch: 0,
|
||||||
|
rng_seed,
|
||||||
|
};
|
||||||
|
sim.start_loop(cx);
|
||||||
|
sim
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lanza el bucle de fondo que avanza la simulación.
|
||||||
|
fn start_loop(&self, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn(async move |this, cx| loop {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(TICK_MS))
|
||||||
|
.await;
|
||||||
|
let alive = this.update(cx, |sim, cx| {
|
||||||
|
if sim.running {
|
||||||
|
sim.advance();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if alive.is_err() {
|
||||||
|
break; // la entidad murió → ventana cerrada.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un paso de simulación; re-siembra si la población colapsa.
|
||||||
|
fn advance(&mut self) {
|
||||||
|
tick(&mut self.world, &self.params);
|
||||||
|
self.tick += 1;
|
||||||
|
if self.world.lemmings.is_empty() {
|
||||||
|
self.epoch += 1;
|
||||||
|
self.rng_seed = self.rng_seed.wrapping_mul(2862933555777941757).wrapping_add(1);
|
||||||
|
self.world = seed(self.rng_seed);
|
||||||
|
self.tick = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-siembra el mundo a mano (botón ↺).
|
||||||
|
fn reseed(&mut self) {
|
||||||
|
self.rng_seed = self.rng_seed.wrapping_add(0x9E3779B9);
|
||||||
|
self.world = seed(self.rng_seed);
|
||||||
|
self.tick = 0;
|
||||||
|
self.epoch += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calcula las estadísticas del instante actual.
|
||||||
|
fn stats(&self) -> Stats {
|
||||||
|
let g = &self.world.grid;
|
||||||
|
Stats {
|
||||||
|
poblacion: self.world.lemmings.len(),
|
||||||
|
materia: g.materia.iter().sum(),
|
||||||
|
oro: g.oro.iter().sum(),
|
||||||
|
energia: self.world.lemmings.energia.iter().sum(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila etiqueta/valor del panel de estadísticas.
|
||||||
|
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_between()
|
||||||
|
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
|
||||||
|
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Sim {
|
||||||
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let panel = hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
|
||||||
|
let chip = hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
|
||||||
|
let canvas_bg = hsla(220.0 / 360.0, 0.22, 0.06, 1.0);
|
||||||
|
let accent = theme.accent;
|
||||||
|
let stats = self.stats();
|
||||||
|
|
||||||
|
// --- Barra de estado ---
|
||||||
|
let estado = if self.running { "● corriendo" } else { "‖ en pausa" };
|
||||||
|
let status = div()
|
||||||
|
.h(px(34.))
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"dominium · campo medio · época {} · tick {}",
|
||||||
|
self.epoch, self.tick
|
||||||
|
)))
|
||||||
|
.child(div().text_color(accent).child(SharedString::from(estado.to_string())));
|
||||||
|
|
||||||
|
// --- Maqueta isométrica ---
|
||||||
|
let plan = build_plan(&self.world, &self.iso, &self.weights, &self.cfg);
|
||||||
|
let canvas = div()
|
||||||
|
.flex_1()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(DominiumCanvas::new(plan).background(canvas_bg));
|
||||||
|
|
||||||
|
// --- Botones de control ---
|
||||||
|
let play_label = if self.running { "‖ Pausar" } else { "▶ Reanudar" };
|
||||||
|
let play = div()
|
||||||
|
.id("play")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child(SharedString::from(play_label.to_string()))
|
||||||
|
.on_click(cx.listener(|sim, _ev, _w, cx| {
|
||||||
|
sim.running = !sim.running;
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
let reset = div()
|
||||||
|
.id("reset")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("↺ Re-sembrar")
|
||||||
|
.on_click(cx.listener(|sim, _ev, _w, cx| {
|
||||||
|
sim.reseed();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Panel de estadísticas ---
|
||||||
|
let side = div()
|
||||||
|
.w(px(216.))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.))
|
||||||
|
.p(px(12.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(div().text_color(theme.fg_muted).child("[SIM]"))
|
||||||
|
.child(play)
|
||||||
|
.child(reset)
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(stat_row("Población", format!("{}", stats.poblacion), &theme))
|
||||||
|
.child(stat_row("Materia", format!("{:.0}", stats.materia), &theme))
|
||||||
|
.child(stat_row("Oro", format!("{:.0}", stats.oro), &theme))
|
||||||
|
.child(stat_row("Energía", format!("{:.0}", stats.energia), &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("grilla {GRID}×{GRID}"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child("relieve = materia (Z)"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Composición ---
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(status)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.flex_1()
|
||||||
|
.child(canvas)
|
||||||
|
.child(side),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch_app("brahman · dominium", (1120., 720.), Sim::new);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "fana"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "fana — editor de escritura DAG: ventana GPUI con el documento como grafo de átomos narrativos, conectores de dependencia y osciloscopio de coherencia."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fana"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fana-core = { path = "../../modules/fana/fana-core" }
|
||||||
|
fana-graph = { path = "../../modules/fana/fana-graph" }
|
||||||
|
fana-render-plan = { path = "../../modules/fana/fana-render-plan" }
|
||||||
|
fana-editor-gpui = { path = "../../modules/fana/fana-editor-gpui" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
gpui = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
//! `fana` — el editor de escritura DAG, ventana GPUI.
|
||||||
|
//!
|
||||||
|
//! Compone la cadena de fana:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! fana-core ─► fana-graph ─► fana-render-plan ─►
|
||||||
|
//! fana-editor-gpui ─► [esta ventana]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! El documento no es un texto plano sino un grafo de átomos
|
||||||
|
//! narrativos. La ventana lo muestra en columnas por rama, con los
|
||||||
|
//! conectores de dependencia y el osciloscopio de coherencia. El botón
|
||||||
|
//! «Mutar raíz» reescribe el átomo origen y dispara la onda de choque
|
||||||
|
//! lógica: todo descendiente cae a «por evaluar».
|
||||||
|
|
||||||
|
use fana_core::{CoherenceState, NarrativeAtom};
|
||||||
|
use fana_editor_gpui::{editor_view, tone_color};
|
||||||
|
use fana_graph::NarrativeGraph;
|
||||||
|
use fana_render_plan::{build_plan, CoherenceTone, LayoutConfig};
|
||||||
|
use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
|
||||||
|
use nahual_launcher::launch_app;
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Estado del editor.
|
||||||
|
struct Fana {
|
||||||
|
graph: NarrativeGraph,
|
||||||
|
/// Átomo raíz — el que muta el botón de demostración.
|
||||||
|
root: Uuid,
|
||||||
|
/// Cuántas veces se mutó la raíz (para variar el texto nuevo).
|
||||||
|
mutations: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fana {
|
||||||
|
fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
|
let (graph, root) = seed_document();
|
||||||
|
Self { graph, root, mutations: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reescribe la raíz y propaga la onda de choque a sus descendientes.
|
||||||
|
fn mutate_root(&mut self) {
|
||||||
|
self.mutations += 1;
|
||||||
|
let nuevo = format!(
|
||||||
|
"Capítulo 1 — versión {}: el viajero nunca llegó al puerto.",
|
||||||
|
self.mutations
|
||||||
|
);
|
||||||
|
if let Some(atom) = self.graph.get_mut(self.root) {
|
||||||
|
atom.set_content(nuevo); // marca la raíz como PendingEvaluation
|
||||||
|
}
|
||||||
|
// Marca en cascada todo descendiente transitivo.
|
||||||
|
self.graph.propagate_mutation(self.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve todos los átomos a estado coherente.
|
||||||
|
fn revalidate(&mut self) {
|
||||||
|
let ids: Vec<Uuid> = self.graph.atoms().map(|a| a.id).collect();
|
||||||
|
for id in ids {
|
||||||
|
if let Some(atom) = self.graph.get_mut(id) {
|
||||||
|
atom.coherence = CoherenceState::Valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuenta átomos en cada estado de coherencia: `(pendientes, conflictos)`.
|
||||||
|
fn coherence_counts(&self) -> (usize, usize) {
|
||||||
|
let mut pending = 0;
|
||||||
|
let mut conflict = 0;
|
||||||
|
for a in self.graph.atoms() {
|
||||||
|
match a.coherence {
|
||||||
|
CoherenceState::PendingEvaluation => pending += 1,
|
||||||
|
CoherenceState::InConflict { .. } => conflict += 1,
|
||||||
|
CoherenceState::Valid => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(pending, conflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye el documento de ejemplo: un relato corto con una rama
|
||||||
|
/// alterna. Devuelve el grafo y el id de la raíz.
|
||||||
|
fn seed_document() -> (NarrativeGraph, Uuid) {
|
||||||
|
let mut root = NarrativeAtom::new(
|
||||||
|
"Capítulo 1 — el viajero llega al puerto al amanecer.",
|
||||||
|
"principal",
|
||||||
|
);
|
||||||
|
root.semantic_vectors.insert("calma".into(), 0.6);
|
||||||
|
let root_id = root.id;
|
||||||
|
|
||||||
|
let mut posada = NarrativeAtom::new(
|
||||||
|
"El posadero le ofrece cuarto y un vaso de vino tibio.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(root_id);
|
||||||
|
posada.semantic_vectors.insert("calma".into(), 0.4);
|
||||||
|
posada.semantic_vectors.insert("misterio".into(), 0.3);
|
||||||
|
let posada_id = posada.id;
|
||||||
|
|
||||||
|
let mut pasos = NarrativeAtom::new(
|
||||||
|
"Por la noche escucha pasos lentos en el pasillo.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(posada_id);
|
||||||
|
pasos.semantic_vectors.insert("misterio".into(), 0.9);
|
||||||
|
pasos.semantic_vectors.insert("miedo".into(), 0.7);
|
||||||
|
let pasos_id = pasos.id;
|
||||||
|
|
||||||
|
let mut puerta = NarrativeAtom::new(
|
||||||
|
"Al amanecer, la puerta de su cuarto está entreabierta.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(pasos_id);
|
||||||
|
puerta.semantic_vectors.insert("miedo".into(), 1.0);
|
||||||
|
puerta.coherence = CoherenceState::InConflict {
|
||||||
|
origin: pasos_id,
|
||||||
|
reason: "el amanecer ya se narró en el capítulo siguiente".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rama alterna: el viajero rechaza la posada.
|
||||||
|
let mut muelle = NarrativeAtom::new(
|
||||||
|
"Pero el viajero rechaza el cuarto y duerme sobre el muelle.",
|
||||||
|
"alterna",
|
||||||
|
)
|
||||||
|
.depends_on(posada_id);
|
||||||
|
muelle.semantic_vectors.insert("soledad".into(), 0.8);
|
||||||
|
|
||||||
|
let graph = NarrativeGraph::from_atoms([root, posada, pasos, puerta, muelle]);
|
||||||
|
(graph, root_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila de leyenda: muestra el color de un tono y su etiqueta.
|
||||||
|
fn legend_row(tone: CoherenceTone, label: &str, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.))
|
||||||
|
.child(div().w(px(12.)).h(px(12.)).rounded(px(3.)).bg(tone_color(tone)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(label.to_string())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila etiqueta/valor del panel.
|
||||||
|
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_between()
|
||||||
|
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
|
||||||
|
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Fana {
|
||||||
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let panel = gpui::hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
|
||||||
|
let chip = gpui::hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
|
||||||
|
let (pending, conflict) = self.coherence_counts();
|
||||||
|
|
||||||
|
let plan = build_plan(&self.graph, &LayoutConfig::default());
|
||||||
|
|
||||||
|
// --- Barra de estado ---
|
||||||
|
let status = div()
|
||||||
|
.h(px(34.))
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child("fana · editor de escritura DAG")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("{} átomos", self.graph.len()))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Lienzo del editor (con scroll) ---
|
||||||
|
let canvas = div()
|
||||||
|
.id("editor-scroll")
|
||||||
|
.flex_1()
|
||||||
|
.overflow_x_scroll()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(editor_view(&plan, &theme));
|
||||||
|
|
||||||
|
// --- Botones (los listeners se cablean abajo con cx.listener) ---
|
||||||
|
let btn_mutar = div()
|
||||||
|
.id("mutar")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("⚡ Mutar raíz")
|
||||||
|
.on_click(cx.listener(|fana, _ev, _w, cx| {
|
||||||
|
fana.mutate_root();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
let btn_revalidar = div()
|
||||||
|
.id("revalidar")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("✓ Re-validar todo")
|
||||||
|
.on_click(cx.listener(|fana, _ev, _w, cx| {
|
||||||
|
fana.revalidate();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Panel lateral ---
|
||||||
|
let side = div()
|
||||||
|
.w(px(240.))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.))
|
||||||
|
.p(px(12.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(div().text_color(theme.fg_muted).child("[DOCUMENTO]"))
|
||||||
|
.child(btn_mutar)
|
||||||
|
.child(btn_revalidar)
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(stat_row("Átomos", format!("{}", self.graph.len()), &theme))
|
||||||
|
.child(stat_row("Por evaluar", format!("{pending}"), &theme))
|
||||||
|
.child(stat_row("En conflicto", format!("{conflict}"), &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(div().text_color(theme.fg_muted).child("coherencia"))
|
||||||
|
.child(legend_row(CoherenceTone::Valid, "coherente", &theme))
|
||||||
|
.child(legend_row(CoherenceTone::Pending, "por evaluar", &theme))
|
||||||
|
.child(legend_row(CoherenceTone::Conflict, "en conflicto", &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(
|
||||||
|
"«Mutar raíz» reescribe el átomo origen: la onda \
|
||||||
|
de choque marca cada descendiente como «por \
|
||||||
|
evaluar».",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Composición ---
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(status)
|
||||||
|
.child(div().flex().flex_row().flex_1().child(canvas).child(side))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch_app("brahman · fana", (1180., 760.), Fana::new);
|
||||||
|
}
|
||||||
@@ -11,9 +11,10 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
|
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
|
||||||
pluma-reader-web = { path = "../../modules/pluma/pluma-reader-web" }
|
fana-md-reader-web = { path = "../../modules/fana/fana-md-reader-web" }
|
||||||
vista-web = { path = "../../modules/vista/vista-web" }
|
revista-web = { path = "../../modules/revista/revista-web" }
|
||||||
barra-web = { path = "../../modules/barra/barra-web" }
|
barra-web = { path = "../../modules/barra/barra-web" }
|
||||||
|
gioser-graph-web = { path = "../../modules/gioser/gioser-graph-web" }
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
wasm-bindgen-futures.workspace = true
|
wasm-bindgen-futures.workspace = true
|
||||||
js-sys.workspace = true
|
js-sys.workspace = true
|
||||||
@@ -38,4 +39,6 @@ features = [
|
|||||||
"NodeList",
|
"NodeList",
|
||||||
"Performance",
|
"Performance",
|
||||||
"console",
|
"console",
|
||||||
|
"Location",
|
||||||
|
"History",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
title: Mística · Espiritualidad
|
||||||
|
camino: uku
|
||||||
|
tags: [mística, espiritualidad, meditación, contemplación]
|
||||||
|
---
|
||||||
|
|
||||||
|
> *La práctica como puente. El misterio como interlocutor.*
|
||||||
|
|
||||||
|
Acá vive lo místico, lo espiritual, las prácticas que sostienen la
|
||||||
|
atención. No es decoración: es la otra mitad del trabajo. Sin esto,
|
||||||
|
el resto se vuelve ruido.
|
||||||
|
|
||||||
|
## Prácticas
|
||||||
|
|
||||||
|
Lo que sostiene día a día:
|
||||||
|
|
||||||
|
- **Meditación.** Sentarse a observar lo que sucede, sin agarrarlo.
|
||||||
|
- **Lectura contemplativa.** Textos que se vuelven a leer hasta que
|
||||||
|
cambian.
|
||||||
|
- **Ceremonia.** Marcar inicios y cierres con gestos que pesan.
|
||||||
|
- **Naturaleza.** Estar en lugares donde uno no es el centro.
|
||||||
|
- **Silencio.** Día completo, una vez por mes mínimo.
|
||||||
|
|
||||||
|
## Por qué mística
|
||||||
|
|
||||||
|
Porque la racionalidad sola no alcanza para vivir. Y porque las
|
||||||
|
tradiciones llevan miles de años elaborando vocabulario para lo que
|
||||||
|
nos pasa cuando atendemos en serio: contemplación, ego, símbolo,
|
||||||
|
muerte, asombro.
|
||||||
|
|
||||||
|
**Mística aplicada** = no quedarse en el libro. Pasar por el cuerpo,
|
||||||
|
por la relación, por la vida cotidiana.
|
||||||
|
|
||||||
|
## Lo que leo
|
||||||
|
|
||||||
|
Andino, budista, cristiano-contemplativo, hindú, sufí. Sin
|
||||||
|
exclusividad: cada tradición resuelve algunas cosas mejor que otras.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Acá se va a ir armando una bitácora de lecturas, prácticas y notas.*
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Software · Tecnología
|
||||||
|
camino: logos
|
||||||
|
tags: [software, tecnología, open-source, rust, python]
|
||||||
|
---
|
||||||
|
|
||||||
|
> *Lo público. Lo que se mantiene abierto.*
|
||||||
|
|
||||||
|
Acá viven los proyectos de **software libre**, herramientas, librerías
|
||||||
|
y exploraciones técnicas que voy publicando. La premisa es mantener
|
||||||
|
el código abierto, documentado y útil más allá del autor.
|
||||||
|
|
||||||
|
## Qué vas a encontrar acá
|
||||||
|
|
||||||
|
- Repos públicos de cosas que escribo (Rust, Python, embedded, web).
|
||||||
|
- Notas técnicas sobre arquitectura, sistemas distribuidos, runtimes.
|
||||||
|
- Ensayos sobre **IA aplicada** — sin hype, con ejemplos concretos.
|
||||||
|
- Bitácoras de exploración: lo que probé, lo que descarté, lo que sigo
|
||||||
|
usando.
|
||||||
|
|
||||||
|
## Por qué open source
|
||||||
|
|
||||||
|
Porque el conocimiento técnico se multiplica cuando circula. Y porque
|
||||||
|
mucho de lo que uso a diario me lo regaló alguien que decidió compartir.
|
||||||
|
La reciprocidad importa.
|
||||||
|
|
||||||
|
## Stack actual
|
||||||
|
|
||||||
|
- **Rust** para lo que necesita ser rápido, seguro y portable.
|
||||||
|
- **Python** para análisis, ML y prototipos rápidos.
|
||||||
|
- **Linux** (Artix/Arch) como sistema operativo de trabajo.
|
||||||
|
- **gitea** + **nix** para infraestructura personal.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Voy a ir enlazando proyectos específicos acá: tools, runtimes,
|
||||||
|
experimentos. Por ahora, este placeholder vive en `docs/aire.md`.*
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: Cosmovisión · Los 4 Elementos
|
||||||
|
camino: cosmos
|
||||||
|
tags: [cosmovisión, elementos, arquetipos, tierra, agua, aire, fuego, marco]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cosmovisión · Los 4 Elementos
|
||||||
|
|
||||||
|
> *Tierra, Agua, Aire, Fuego. No son decoración. Son la sintaxis del estudio.*
|
||||||
|
|
||||||
|
Este camino es el marco conceptual que organiza los cuatro caminos
|
||||||
|
originales. Los elementos no son metáforas poéticas: son arquetipos
|
||||||
|
operativos que describen modos de ser, hacer y relacionarse.
|
||||||
|
|
||||||
|
## Los Cuatro Caminos
|
||||||
|
|
||||||
|
### Tierra (kay)
|
||||||
|
|
||||||
|
Lo que se sostiene. Lo firme. El cuerpo, la materia, la invariante.
|
||||||
|
|
||||||
|
El camino de Tierra es el manifiesto: las axiomas que no cambian, el
|
||||||
|
protocolo de Presencia, el algoritmo de perdón. Es la base del sistema.
|
||||||
|
Sin Tierra, los demás elementos flotan sin anclaje.
|
||||||
|
|
||||||
|
Tierra pregunta: *¿Qué es verdad aunque todo lo demás cambie?*
|
||||||
|
|
||||||
|
### Agua (uku)
|
||||||
|
|
||||||
|
Lo que fluye. La emoción, la intuición, el vínculo con el misterio.
|
||||||
|
|
||||||
|
El camino de Agua es la mística: la espiritualidad aplicada, la
|
||||||
|
meditación, la ceremonia, el silencio. No es adorno: es el disolvente
|
||||||
|
que evita que el sistema se cristalice en dogma.
|
||||||
|
|
||||||
|
Agua pregunta: *¿Qué se mueve cuando dejo de controlar?*
|
||||||
|
|
||||||
|
### Aire (logos)
|
||||||
|
|
||||||
|
Lo que circula. La información, el código, la conexión.
|
||||||
|
|
||||||
|
El camino de Aire es el software: las herramientas técnicas, los
|
||||||
|
sistemas distribuidos, la IA aplicada, el open source. Es la red
|
||||||
|
que conecta los nodos del estudio.
|
||||||
|
|
||||||
|
Aire pregunta: *¿Qué podemos compartir que multiplique inteligencia?*
|
||||||
|
|
||||||
|
### Fuego (nomos)
|
||||||
|
|
||||||
|
Lo que transforma. La identidad, la bitácora, el testimonio.
|
||||||
|
|
||||||
|
El camino de Fuego es el quién soy: la crónica personal, el
|
||||||
|
testimonio de alguien que practica mantenerse despierto. Es el
|
||||||
|
calor humano que evita que el sistema sea pura abstracción.
|
||||||
|
|
||||||
|
Fuego pregunta: *¿Quién soy cuando dejo de ser lo que aprendí?*
|
||||||
|
|
||||||
|
## El centro
|
||||||
|
|
||||||
|
Los cuatro elementos no son jerárquicos. Son un campo. El centro
|
||||||
|
es la Presencia — el punto fijo desde el cual todos se experimentan.
|
||||||
|
|
||||||
|
*Cada elemento contiene a los otros.* Tierra sin agua se petrifica.
|
||||||
|
Agua sin tierra se desborda. Aire sin fuego es frío. Fuego sin aire
|
||||||
|
se sofoca.
|
||||||
|
|
||||||
|
## Integración
|
||||||
|
|
||||||
|
Los caminos nuevos — cuerpo, sombra, práctica, olvido — no son
|
||||||
|
elementos adicionales. Son **modos de aplicación** de los cuatro
|
||||||
|
elementos en dominios específicos de la experiencia:
|
||||||
|
|
||||||
|
- **Cuerpo**: Tierra + Agua aplicados a la somática
|
||||||
|
- **Sombra**: Agua + Fuego aplicados al inconsciente
|
||||||
|
- **Práctica**: los 4 en acción concreta
|
||||||
|
- **Olvido**: Aire + Tierra en modo de liberación
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Este marco se irá expandiendo con diagramas, referencias a la
|
||||||
|
tradición hermética y aplicaciones concretas de cada elemento en
|
||||||
|
el trabajo diario.*
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: El Cuerpo como Portal
|
||||||
|
camino: cuerpo
|
||||||
|
tags: [somática, respiración, movimiento, cuerpo, presencia, encarnación]
|
||||||
|
---
|
||||||
|
|
||||||
|
# El Cuerpo como Portal
|
||||||
|
|
||||||
|
> *La respiración como ancla. El movimiento como oráculo. La carne como templo.*
|
||||||
|
|
||||||
|
No hay despertar sin cuerpo. La presencia puede pensarse desde la mente,
|
||||||
|
pero se encarna — literalmente — en el cuerpo. Este camino explora la
|
||||||
|
dimensión somática del trabajo interior.
|
||||||
|
|
||||||
|
## Respiración
|
||||||
|
|
||||||
|
La respiración es el puente más directo entre lo voluntario y lo autónomo.
|
||||||
|
Cuando no sabes qué hacer, respirar es siempre una respuesta válida.
|
||||||
|
|
||||||
|
Prácticas que sostienen el trabajo respiratorio:
|
||||||
|
|
||||||
|
- **Respiración diafragmática 4-7-8**: inspira 4", retén 7", exhala 8".
|
||||||
|
Dice el sistema nervioso que se regula solo si se lo dejas claro.
|
||||||
|
- **Pausa espiratoria**: exhalar completo y quedarse vacío unos segundos.
|
||||||
|
El silencio entre respiros es la presencia más limpia.
|
||||||
|
- **Respiración cuadrada**: 4 tiempos iguales (inspirar, retener, exhalar,
|
||||||
|
retener). Sin nombre fancy: simplemente simetría.
|
||||||
|
|
||||||
|
## Movimiento consciente
|
||||||
|
|
||||||
|
El cuerpo no es una carga que la mente debe arrastrar. Es el vehículo de la
|
||||||
|
atención. Moverse con presencia es una práctica completa.
|
||||||
|
|
||||||
|
- **Caminar meditativo**: pasos lentos, sentir el peso que se transfiere,
|
||||||
|
el contacto con el suelo.
|
||||||
|
- **Estiramientos** al despertar: 5 minutos de elongación consciente antes
|
||||||
|
de agarrar el teléfono.
|
||||||
|
- **Sacudidas**: literalmente agitar el cuerpo para resetear el estado
|
||||||
|
nervioso. Los animales lo hacen instintivamente.
|
||||||
|
|
||||||
|
## El cuerpo como sensor
|
||||||
|
|
||||||
|
Las emociones no son conceptuales. Son patrones somáticos: tensión en el
|
||||||
|
pecho, nudo en la garganta, mariposas en el estómago.
|
||||||
|
|
||||||
|
Aprender a leer estas señales es un alfabeto que pocos enseñan:
|
||||||
|
|
||||||
|
1. **Escaneo corporal rápido**: de pies a cabeza, 30 segundos, notar sin
|
||||||
|
cambiar nada.
|
||||||
|
2. **Mapa de tensión**: en qué parte del cuerpo viven mis patrones habituales?
|
||||||
|
Mandíbula? Hombros? Pelvis? Cada cual tiene su geografía.
|
||||||
|
3. **El suspiro como dato**: si suspiras hondo sin pensar, tu cuerpo te está
|
||||||
|
diciendo algo que quizás la mente ignora.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Acá se irá profundizando en técnicas somáticas, ejercicios de presencia
|
||||||
|
corporal y la integración del movimiento como práctica espiritual.*
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: Quién Soy · Bitácora
|
||||||
|
camino: nomos
|
||||||
|
tags: [identidad, bitácora, crónica, personal]
|
||||||
|
---
|
||||||
|
|
||||||
|
> *La identidad como verbo. La crónica como práctica.*
|
||||||
|
|
||||||
|
Acá vive lo personal: quién soy, qué hago, qué leo, qué pienso. Una
|
||||||
|
bitácora honesta, no curada para impresionar. Si vas a leer esto,
|
||||||
|
asumí que es borrador.
|
||||||
|
|
||||||
|
## Quién soy
|
||||||
|
|
||||||
|
**Sergio**. Programador, lector, padre, alguien que practica
|
||||||
|
mantenerse despierto. Vivo entre código, café, montañas y libros.
|
||||||
|
|
||||||
|
Las cosas que más me importan no son las que mejor cuento todavía.
|
||||||
|
Por eso escribo: para precisar lo que sé y lo que no.
|
||||||
|
|
||||||
|
## Bitácora
|
||||||
|
|
||||||
|
Notas más o menos diarias sobre lo que voy pensando, viviendo,
|
||||||
|
fallando. Sin algoritmo de engagement, sin métricas. Sólo crónica.
|
||||||
|
|
||||||
|
Las entradas se ordenan por fecha. Las más viejas a veces dicen cosas
|
||||||
|
que ya no pienso así — las dejo igual.
|
||||||
|
|
||||||
|
## Por qué publicarlo
|
||||||
|
|
||||||
|
Porque escribir en público obliga a precisar. Y porque a veces lo que
|
||||||
|
uno escribe para sí mismo le sirve a otra persona que no conoce.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Acá se va a ir armando una bitácora con entradas fechadas.*
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: El Arte de Olvidar
|
||||||
|
camino: olvido
|
||||||
|
tags: [olvido, desaprender, soltar, memoria, perdón, identidad, vaciar]
|
||||||
|
---
|
||||||
|
|
||||||
|
# El Arte de Olvidar
|
||||||
|
|
||||||
|
> *No se trata de recordar más. Se trata de poder soltar.*
|
||||||
|
|
||||||
|
Olvidar no es fallo de memoria. Es una capacidad activa — quizás la más
|
||||||
|
subestimada del trabajo interior. Este camino explora el desaprender,
|
||||||
|
soltar la identidad, vaciar el caché de la memoria complementando el
|
||||||
|
algoritmo de perdón del Manifiesto.
|
||||||
|
|
||||||
|
## Por qué olvidar es necesario
|
||||||
|
|
||||||
|
La memoria no es un disco duro. Es un proceso reconstructivo que se
|
||||||
|
actualiza cada vez que recordamos. Eso significa que el pasado no está
|
||||||
|
fijo: se reescribe en cada recuperación.
|
||||||
|
|
||||||
|
Si no olvidamos, cargamos con versiones obsoletas de nosotros mismos.
|
||||||
|
El rencor, la identificación con el trauma, la historia personal que
|
||||||
|
repetimos como un mantra — todo eso es memoria no liberada.
|
||||||
|
|
||||||
|
El *Algoritmo de perdón* del Manifiesto describe el protocolo formal.
|
||||||
|
Acá exploramos la práctica cotidiana.
|
||||||
|
|
||||||
|
## Prácticas de olvido activo
|
||||||
|
|
||||||
|
### 1. Reencuadre deliberado
|
||||||
|
|
||||||
|
Cuando una memoria perturbadora aparezca:
|
||||||
|
|
||||||
|
1. Reconócela sin resistencia.
|
||||||
|
2. Respira hondo.
|
||||||
|
3. Pregunta: *¿Esta versión de los hechos sigue siendo cierta?*
|
||||||
|
4. Pregunta: *¿Qué necesitaría soltar para que esta memoria pierda su carga?*
|
||||||
|
5. Reescribe mentalmente la escena desde otra perspectiva — desde afuera,
|
||||||
|
desde el otro, desde el testigo.
|
||||||
|
|
||||||
|
### 2. La ceremonia de cierre
|
||||||
|
|
||||||
|
Para memorias que se niegan a disolverse:
|
||||||
|
|
||||||
|
1. Escribe la memoria en detalle. Todo lo que carga.
|
||||||
|
2. Siéntate con el texto. Léelo en voz alta.
|
||||||
|
3. Quema el papel (con seguridad). O entiérralo. O arrójalo al agua.
|
||||||
|
4. El gesto físico ancla el cambio que la mente sola no completa.
|
||||||
|
|
||||||
|
### 3. Vaciar el caché diario
|
||||||
|
|
||||||
|
Antes de dormir:
|
||||||
|
|
||||||
|
1. Repasa el día como una película rápida.
|
||||||
|
2. Identifica los momentos que todavía tienen carga emocional.
|
||||||
|
3. Respira en cada uno hasta que la carga se normalice.
|
||||||
|
4. Suelta. Literalmente: di "suelto" o exhala con intención.
|
||||||
|
|
||||||
|
### 4. Desaprender una creencia
|
||||||
|
|
||||||
|
Elige una creencia sobre ti mismo que ya no te sirve:
|
||||||
|
|
||||||
|
- "Yo soy tímido"
|
||||||
|
- "Yo no sirvo para X"
|
||||||
|
- "A mí siempre me pasa Y"
|
||||||
|
|
||||||
|
Durante 30 días, actúa *como si* esa creencia no fuera cierta.
|
||||||
|
No necesitas creer lo opuesto. Solo suspender la creencia anterior.
|
||||||
|
|
||||||
|
## Olvido y algoritmo de perdón
|
||||||
|
|
||||||
|
El algoritmo del Manifiesto (escaneo → anclaje en P → remuestreo →
|
||||||
|
reescritura → garbage collection) es la versión técnica. Las prácticas
|
||||||
|
de acá son la versión accesible para el día a día.
|
||||||
|
|
||||||
|
El olvido no es borrar. Es crear espacio para lo nuevo.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Técnicas avanzadas de desidentificación, protocolos de olvido
|
||||||
|
asistido y textos de referencia sobre el tema.*
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: Prácticas de Transformación
|
||||||
|
camino: practica
|
||||||
|
tags: [prácticas, ejercicios, transformación, respiración, meditación, registro, acecho]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prácticas de Transformación
|
||||||
|
|
||||||
|
> *No basta comprender. Hay que hacer. Y hay que hacer de nuevo mañana.*
|
||||||
|
|
||||||
|
Este camino recolecta ejercicios concretos — no teoría, no contemplación,
|
||||||
|
sino prácticas que se hacen. Cada una con instrucciones claras, duración
|
||||||
|
estimada y criterios para saber si funcionó.
|
||||||
|
|
||||||
|
## Respiración consciente (10 min)
|
||||||
|
|
||||||
|
**Objetivo**: anclar la atención en el cuerpo a través del ritmo respiratorio.
|
||||||
|
|
||||||
|
**Instrucciones**:
|
||||||
|
1. Siéntate en una posición cómoda, columna erguida pero no rígida.
|
||||||
|
2. Cierra los ojos o deja la mirada suave en un punto.
|
||||||
|
3. Lleva toda la atención a la sensación del aire entrando y saliendo.
|
||||||
|
Puede ser en las fosas nasales, en el pecho o en el abdomen.
|
||||||
|
4. Cuando la mente se vaya (y se irá), vuelve a traerla. Sin drama.
|
||||||
|
5. Después de unos minutos, cuenta las exhalaciones del 1 al 10.
|
||||||
|
Si pierdes la cuenta, empieza de nuevo.
|
||||||
|
|
||||||
|
**Señal de efectividad**: al terminar, notas que el ritmo cardíaco bajó
|
||||||
|
o que hay una pausa natural entre respiros.
|
||||||
|
|
||||||
|
## Meditación de Presencia (20 min)
|
||||||
|
|
||||||
|
**Objetivo**: habitar P (Presencia) sin objeto de meditación.
|
||||||
|
|
||||||
|
**Instrucciones**:
|
||||||
|
1. Siéntate. Sin buscar un estado especial.
|
||||||
|
2. No sigas la respiración. No repitas un mantra. No visualices nada.
|
||||||
|
3. Solo quédate presente. Sin hacer nada para lograrlo.
|
||||||
|
4. Cuando notes que estás en un pensamiento, simplemente nota: "pensando".
|
||||||
|
Sin juicio. Y suelta.
|
||||||
|
5. El "suelta" no requiere acción. Solo deja de sostener el pensamiento.
|
||||||
|
|
||||||
|
**Nota**: esta práctica es difícil. Sentirás aburrimiento, sueño,
|
||||||
|
inquietud. Todo eso es parte de la práctica. No la estás haciendo mal.
|
||||||
|
|
||||||
|
**Señal de efectividad**: en algún momento hay una pausa donde el mundo
|
||||||
|
se siente más *real* que los pensamientos sobre el mundo.
|
||||||
|
|
||||||
|
## Registro de chips (15 min diarios)
|
||||||
|
|
||||||
|
**Objetivo**: identificar patrones cognitivos recurrentes.
|
||||||
|
|
||||||
|
**Formato**: diario con tres columnas.
|
||||||
|
|
||||||
|
| Fecha | Gatillo | Patrón | Respuesta |
|
||||||
|
|-------|---------|--------|-----------|
|
||||||
|
| hoy | correo del trabajo | "no soy suficiente" | apretar mandíbula |
|
||||||
|
| hoy | discusión con X | "siempre me pasa igual" | retirarme |
|
||||||
|
|
||||||
|
Sin interpretación. Solo registro. Los patrones se vuelven visibles
|
||||||
|
después de 2-3 semanas.
|
||||||
|
|
||||||
|
**Criterio**: si después de un mes no ves patrones, probablemente no
|
||||||
|
estás siendo honesto en el registro.
|
||||||
|
|
||||||
|
## Acecho de un patrón (1 semana)
|
||||||
|
|
||||||
|
**Objetivo**: observar un chip cognitivo sin intervenir.
|
||||||
|
|
||||||
|
1. Elige UN patrón de tu registro de chips. El que más se repita.
|
||||||
|
2. Durante una semana, solo obsérvalo cuando aparezca.
|
||||||
|
3. No intentes cambiarlo. No lo analices. Solo nótalo.
|
||||||
|
4. Anota cada aparición: contexto, intensidad (1-10), duración.
|
||||||
|
|
||||||
|
**Lo que sucede**: sin intervención, el sistema empieza a autorregularse.
|
||||||
|
La mera observación sostenida erosiona el atractor.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Más prácticas: meditación caminando, escaneo corporal guiado,
|
||||||
|
ejercicios de acecho avanzados y protocolos de intervención.*
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Trabajo con la Sombra
|
||||||
|
camino: sombra
|
||||||
|
tags: [sombra, jung, integración, patrones, inconsciente, proyección, acecho]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Trabajo con la Sombra
|
||||||
|
|
||||||
|
> *Lo negado. Lo que no quiero ver. Lo que me gobierna desde la oscuridad.*
|
||||||
|
|
||||||
|
La sombra no es el enemigo. Es todo aquello que la conciencia no ha integrado:
|
||||||
|
los patrones automáticos, las reacciones que juré que no tendría, las partes
|
||||||
|
de mí que niego. Trabajar con la sombra no es eliminarla — es hacerla
|
||||||
|
consciente.
|
||||||
|
|
||||||
|
## ¿Qué es la sombra?
|
||||||
|
|
||||||
|
En el marco de este estudio, la **Sombra (S)** es el conjunto de chips
|
||||||
|
cognitivos no escaneados. Patrones que operan por debajo del umbral de
|
||||||
|
la Presencia (P). No son malos. Son inconscientes.
|
||||||
|
|
||||||
|
Se manifiestan como:
|
||||||
|
|
||||||
|
- **Proyección**: lo que me irrita en otros suele ser lo que no acepto en mí.
|
||||||
|
- **Reactividad automática**: una respuesta desproporcionada que me sorprende
|
||||||
|
a mí mismo.
|
||||||
|
- **Patrones recurrentes**: las mismas situaciones, las mismas personas, los
|
||||||
|
mismos conflictos en bucle.
|
||||||
|
- **Negación**: "eso no soy yo" dicho con demasiada convicción.
|
||||||
|
|
||||||
|
## Protocolo de integración
|
||||||
|
|
||||||
|
No se trata de iluminar cada rincón oscuro con una linterna. Se trata de
|
||||||
|
ampliar la capacidad de albergar contradicción.
|
||||||
|
|
||||||
|
**Fase 1 — Cartografía de activación**
|
||||||
|
|
||||||
|
Llevar un registro de momentos de alta reactividad emocional. Anotar:
|
||||||
|
qué pasó, qué sentí, qué pensé, qué hice. Sin juzgar. Sin interpretar.
|
||||||
|
Solo registrar.
|
||||||
|
|
||||||
|
Con el tiempo emergen patrones.
|
||||||
|
|
||||||
|
**Fase 2 — Acecho**
|
||||||
|
|
||||||
|
Elegir un patrón identificado y observarlo activamente durante una semana.
|
||||||
|
No intervenir. Solo notar: cuándo aparece, qué lo gatilla, cómo se siente
|
||||||
|
en el cuerpo.
|
||||||
|
|
||||||
|
Acechar no es cazar. Es conocer el territorio.
|
||||||
|
|
||||||
|
**Fase 3 — Conversación con la sombra**
|
||||||
|
|
||||||
|
Técnica: escribir un diálogo con la parte rechazada. Ponerle voz. Preguntarle
|
||||||
|
qué necesita, qué protege, qué quiere. No asumir que sabes la respuesta.
|
||||||
|
|
||||||
|
**Fase 4 — Integración**
|
||||||
|
|
||||||
|
Encontrar un lugar funcional para esa parte en la vida consciente. No es
|
||||||
|
eliminarla: es darle un asiento en la mesa. A veces lo que la sombra protege
|
||||||
|
es legítimo — solo se expresaba mal.
|
||||||
|
|
||||||
|
## Herramientas cotidianas
|
||||||
|
|
||||||
|
- **El espejo**: preguntar a alguien de confianza "¿qué patrón ves en mí que
|
||||||
|
yo no veo?"
|
||||||
|
- **La pausa**: cuando sientas reactividad, no actuar. Esperar 24 horas antes
|
||||||
|
de responder.
|
||||||
|
- **El diario de sueños**: los sueños a menudo expresan material de sombra
|
||||||
|
que la vigilia censura.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Se irán añadiendo ejercicios específicos, técnicas gestálticas y referencias
|
||||||
|
a autores que han trabajado este terreno (Jung, Perls, Bly, Zweig).*
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
title: Modelo Triplanar — Ontología del Campo Consciente
|
||||||
|
camino: tierra
|
||||||
|
tags: [manifiesto, triplanar, ontologia, planos, pipeline]
|
||||||
|
---
|
||||||
|
# Modelo Triplanar
|
||||||
|
|
||||||
|
## Ontología del Campo Consciente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I. Tesis Fundamental
|
||||||
|
|
||||||
|
El primer grado de evidencia es la propia existencia. Desde ese punto de partida: el universo mismo es consciente. La conciencia no es un epifenómeno que emerge en sistemas complejos — es una propiedad del campo en sí mismo.
|
||||||
|
|
||||||
|
Todo es consciente. Cada posición del plano multidimensional es un nodo de consciencia con su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
|
||||||
|
|
||||||
|
No hay una "realidad base" que contenga a los planos. Son ontologías paralelas, cada una completa y verdadera desde su propia posición.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## II. Plano 1 — Sensible
|
||||||
|
|
||||||
|
<code>
|
||||||
|
S₁ = {afección sensorial pura}
|
||||||
|
∄ "yo" en S₁
|
||||||
|
∄ cuerpo en S₁
|
||||||
|
S₁ solo contiene datos: color, sonido, textura, temperatura
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Lo que hay: el toque de los sentidos, sin cuerpo que los reporte. El cuerpo físico como tal no existe desde aquí. Hay afección sensible pura — colores, sonidos, texturas — sin un "yo" al que pertenezcan.
|
||||||
|
|
||||||
|
La reconstrucción del cuerpo es posterior y ocurre en otro plano (§III). Para el plano sensible solo hay datos sensoriales sin un centro que los unifique.
|
||||||
|
|
||||||
|
El tiempo se experimenta como flujo sucesivo. El instante presente es un punto de fuga entre lo que fue y lo que será — nunca se puede "agarrar".
|
||||||
|
|
||||||
|
La constatación física en su nivel más burdo: el cuerpo como evidencia inmediata con correlato material. Los pensamientos como circuitos eléctricos y químicos funcionando como mecanismo determinista.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** solo existe la afección sensible. El tiempo es real como sucesión. El cuerpo no está presente como tal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Plano 2 — Alma / Mental
|
||||||
|
|
||||||
|
<code>
|
||||||
|
S₂ = {imágenes mentales, conceptualizaciones, campo relacional}
|
||||||
|
cuerpo(S₂) = imagen mental del cuerpo, no carne
|
||||||
|
t(S₂) = objeto bloque (pasado, presente, futuro coexisten)
|
||||||
|
campo(S₂) = f(interacción de nodos de consciencia)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Lo que hay: la imagen mental de las cosas. Aquí viven las conceptualizaciones.
|
||||||
|
|
||||||
|
El cuerpo no existe como carne — existe como imagen mental del cuerpo. Es una reconstrucción desde los datos del plano sensible.
|
||||||
|
|
||||||
|
El alma surge de la organización e interrelación masiva de distintos puntos del universo. La interacción de nodos de consciencia genera un campo. Dos vías que convergen al mismo resultado:
|
||||||
|
|
||||||
|
- Si el campo del alma ya existe independiente de la tierra → se hereda
|
||||||
|
- Si no existe → se genera desde la interacción
|
||||||
|
|
||||||
|
El efecto final es el mismo: dos planos coexistiendo (S₁ + S₂).
|
||||||
|
|
||||||
|
### Tiempo
|
||||||
|
|
||||||
|
Aquí el tiempo ya no es flujo vivido. Es una cosa contenida. Toda la línea temporal se puede ver como una forma — pasado, presente y futuro coexistiendo como bloque. El alma puede ver la dimensión completa del tiempo desde fuera.
|
||||||
|
|
||||||
|
### El conflicto humano
|
||||||
|
|
||||||
|
<code>
|
||||||
|
pecado original = conflicto de transición entre zonas biológicas
|
||||||
|
no es moral — es el bamboleo de una especie entre dos equilibrios
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El "pecado original" no es una mancha moral. Es la descripción del conflicto que aparece cuando una especie animal se asoma fuera de la corriente de la selva sin haber terminado el movimiento.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** el alma existe como campo. El tiempo es un objeto que se puede contemplar. El cuerpo es solo imagen mental.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. Plano 3 — Presencia (0, 0)
|
||||||
|
|
||||||
|
<code>
|
||||||
|
S₃ = {fenomenología pura}
|
||||||
|
P = (0, 0, 0)
|
||||||
|
∀s ∈ S : s − P = v
|
||||||
|
t(S₃) = ∅ // el tiempo no es categoría aplicable
|
||||||
|
S₃ es un punto que contiene el universo completo
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Lo que hay: fenomenología pura. Un punto es el universo completo, porque todo lo demás es solo contenido de su visión.
|
||||||
|
|
||||||
|
El Nivel 1 de evidencia solo reporta la propia existencia — y eso es suficiente. Presencia como origen del sistema de coordenadas: (0, 0).
|
||||||
|
|
||||||
|
El tiempo aquí no existe. Solo hay instante presente, y ese instante es lo único que hay. No es que "se esté en el presente" — el presente es la totalidad. La sucesión ni siquiera es una categoría aplicable.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** solo existe el instante presente, y ese instante lo contiene todo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Ontología de la Vida
|
||||||
|
|
||||||
|
### La vida no es delimitable
|
||||||
|
|
||||||
|
Lo que comúnmente llamamos "vida" es sesgo de cercanía — lo que más se parece a nuestra forma de vida. Pero todo es vida, en diferentes expresiones:
|
||||||
|
|
||||||
|
- Unas se reproducen, otras no
|
||||||
|
- Unas son piedras, otras son bacterias, otras son inteligencia artificial
|
||||||
|
- La vida es la misma actividad de las partículas siguiendo su libertad de elección con forma partícula/onda
|
||||||
|
|
||||||
|
### Zonas biológicas
|
||||||
|
|
||||||
|
Dentro del mundo biológico existen "zonas" que cumplen leyes particulares donde coinciden:
|
||||||
|
|
||||||
|
- La influencia del hábitat
|
||||||
|
- La herencia genética
|
||||||
|
- Los eventos contingentes
|
||||||
|
|
||||||
|
La zona más cercana a nosotros es entre la tierra y el cielo. Es un lugar de dualidad que forma cuerpos con pies, cabeza, peso y la necesidad de hacer ejercicio.
|
||||||
|
|
||||||
|
### Postura erecta
|
||||||
|
|
||||||
|
<code>
|
||||||
|
pararse en dos pies = sistema de equilibrio en desequilibrio permanente
|
||||||
|
no es detalle biomecánico — es la forma física del desequilibrio
|
||||||
|
el cuerpo se sostiene cayendo hacia adelante y recuperándose
|
||||||
|
</code>
|
||||||
|
|
||||||
|
En algún punto de la evolución, la especie animal asomó la cabeza del mundo animal y comenzó a entreverse donde se es humano. No hay diferencias de valor entre zonas — solo zonas distintas. Pero la especie aún no se ha actualizado: empezó un viaje donde atraviesa un desequilibrio para volver a un nuevo equilibrio.
|
||||||
|
|
||||||
|
### El ego como síntoma
|
||||||
|
|
||||||
|
<code>
|
||||||
|
ego = mareo del viaje entre dos zonas
|
||||||
|
= síntoma de estar entre: vista fuera de la selva,
|
||||||
|
cuerpo aún no reequilibrado en la nueva posición
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El ego no es error ni enfermedad. Es el mareo del viaje que enfrentó una especie animal incursionando en la humanidad. Es el síntoma de estar entre dos zonas: la vista ya no está en la selva, pero el cuerpo aún no se reequilibró en la nueva posición.
|
||||||
|
|
||||||
|
El conflicto humano (pecado original, §III) es el bamboleo de ese pasaje.
|
||||||
|
|
||||||
|
Las distintas culturas, sistemas morales, tecnologías — son intentos parciales de encontrar el nuevo piso. La especie sigue en el loop del desequilibrio, generando versiones temporales de lo humano que intentan estabilizarse sin lograrlo del todo. El movimiento no ha terminado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. Resumen
|
||||||
|
|
||||||
|
| Plano | Contenido | Tiempo | Cuerpo | Verdad desde su posición |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1. Sensible | Afección sensorial pura | Flujo sucesivo | No existe — solo datos | Solo existe el toque de los sentidos |
|
||||||
|
| 2. Alma/Mental | Imagen mental, conceptualizaciones, campo | Bloque (visible desde fuera) | Imagen mental del cuerpo | Solo existe el alma y sus reconstrucciones |
|
||||||
|
| 3. Presencia (0,0) | Fenomenología pura | No-tiempo / solo instante presente | No aplica | Solo existe el presente que lo contiene todo |
|
||||||
|
|
||||||
|
Cada plano es una verdad absoluta de por sí. No hay contradicción entre ellos — son verdades de posiciones distintas en el campo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Operadores prácticos
|
||||||
|
|
||||||
|
A continuación, herramientas que operan dentro de los planos — principalmente en S₂, donde ocurren los chips cognitivos y las conceptualizaciones.
|
||||||
|
|
||||||
|
### Chip cognitivo (Cᵢ)
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Cᵢ ⊂ S₂ = atractor local en el campo mental
|
||||||
|
bucle de retroalimentación que estabiliza patrones subóptimos
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Ejemplos: rumiación, diálogo interno autoalimentado, pánico.
|
||||||
|
|
||||||
|
### Pipeline
|
||||||
|
|
||||||
|
1. **Detección**: identificar el patrón recurrente en S₂. T (testigo desde S₃) puede detectar sin intervenir.
|
||||||
|
2. **Pausa del script**: T interrumpe la ejecución automática. El bucle se corta al dejar de identificarse con él.
|
||||||
|
3. **Modulación**: herramientas para bajar la ganancia del sistema (respiración, movimiento, frío, silencio).
|
||||||
|
4. **Redirección plástica**: prácticas sostenidas que refuerzan nuevas rutas.
|
||||||
|
5. **Mantenimiento**: rutinas preventivas, análisis de triggers.
|
||||||
|
|
||||||
|
### Métrica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Co = señal / ruido psicofisiológico
|
||||||
|
H = H(señal conductual) // entropía temporal
|
||||||
|
IR = t(baseline | perturbación)
|
||||||
|
PA = Δ(IR, Co) tras intervención
|
||||||
|
</code>
|
||||||
|
|
||||||
|
### Protocolo
|
||||||
|
|
||||||
|
**Diario**: reactividad (0-10), duración, intensidad (0-10).
|
||||||
|
|
||||||
|
**Escalonamiento**: detección → pausa → modulación → consolidación → mantenimiento.
|
||||||
|
|
||||||
|
**Auditoría**: cada 4-12 semanas — Co, IR, H, PA. Sin mejora → ajustar.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Manifiesto · Invariantes
|
||||||
|
camino: kay
|
||||||
|
tags: [manifiesto, principios, invariantes, ética]
|
||||||
|
---
|
||||||
|
|
||||||
|
> *Lo que no cambia. La piedra de toque.*
|
||||||
|
|
||||||
|
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
|
||||||
|
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
|
||||||
|
antes que cualquier proyecto particular.
|
||||||
|
|
||||||
|
## Invariantes
|
||||||
|
|
||||||
|
Cosas que considero **no-negociables** en cómo hago el trabajo:
|
||||||
|
|
||||||
|
- **Código abierto por defecto.** Si tiene sentido, se publica.
|
||||||
|
- **Honestidad por encima de marketing.** No prometo lo que no puedo
|
||||||
|
cumplir, ni vendo lo que no probé.
|
||||||
|
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
|
||||||
|
opuesto al trabajo. Sin cuerpo no hay nada.
|
||||||
|
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
|
||||||
|
no existe la idea.
|
||||||
|
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
|
||||||
|
duran, las modas no.
|
||||||
|
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
|
||||||
|
|
||||||
|
## Por qué un manifiesto
|
||||||
|
|
||||||
|
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
|
||||||
|
principios reduce la energía gastada en cada elección — y deja en
|
||||||
|
claro cuándo estoy contradiciéndome.
|
||||||
|
|
||||||
|
## Revisión
|
||||||
|
|
||||||
|
Este manifiesto se revisa una vez al año, no antes. Si una invariante
|
||||||
|
deja de aplicarse, se quita con una explicación pública.
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
---
|
||||||
|
title: Manifiesto del Ser Desnudo
|
||||||
|
camino: tierra
|
||||||
|
tags: [manifiesto, ciencia-autoexperimental, axiomas, practica, metrica, pipeline]
|
||||||
|
---
|
||||||
|
# Manifiesto del Ser Desnudo
|
||||||
|
|
||||||
|
## I. El Origen: Nacer Humano y Desnudo
|
||||||
|
|
||||||
|
Existir no es un accidente de la marea. Es el acto supremo de una voluntad que ha elegido estar aquí.
|
||||||
|
|
||||||
|
Naciste porque quisiste nacer. Mereces esta bendición que es la existencia por el simple hecho de respirar. No has de hacer nada más.
|
||||||
|
|
||||||
|
Eres la semilla. Eres el puente viviente entre el misterio y la materia. Echa raíces profundas en la Madre Tierra. Levanta tu columna recta hacia el Padre Cielo.
|
||||||
|
|
||||||
|
En esa verticalidad, tú eres el equilibrio. Eres un dios caminando, magnífico en tu propia fragilidad.
|
||||||
|
|
||||||
|
Reconoce tu pequeña luz humana: eres como un infante que fantasea que ya creció, petulante al negar el suelo bajo sus pies, pero perfecto en su inmadurez.
|
||||||
|
|
||||||
|
> *No somos un ser que se transforma. Somos un transformar que se es.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## II. Del Cuerpo Formal
|
||||||
|
|
||||||
|
Que la poesía no nos distraiga del rigor. Si la experiencia es un territorio, necesita mapa. No un mapa que pretenda ser el territorio — eso es idolatría — sino uno que permita navegarlo con precisión.
|
||||||
|
|
||||||
|
### Axioma 1 — Presencia como origen
|
||||||
|
|
||||||
|
Existe un punto de referencia universal en el espacio experiencial, llamado **Presencia** (P). Se define operacionalmente como el punto de auto-evidencia en el que la experiencia se registra sin identificarse con ella.
|
||||||
|
|
||||||
|
En coordenadas experienciales: **P = (0, 0, 0)**. Todo vector se mide desde aquí. No hay afuera de P porque P es el punto desde el cual todo afuera se define.
|
||||||
|
|
||||||
|
### Axioma 2 — Separación procesual
|
||||||
|
|
||||||
|
La experiencia se compone de dos capas:
|
||||||
|
|
||||||
|
- **Observador (O)**: idéntico a P. El testigo.
|
||||||
|
- **Flujo de datos (D)**: el conjunto de variables sensibles — pensamientos, emociones, percepciones, sensaciones corporales.
|
||||||
|
|
||||||
|
La independencia funcional se escribe: **O ∩ D = ∅**. No eres tus pensamientos. No es una metáfora: es una condición del sistema.
|
||||||
|
|
||||||
|
### Axioma 3 — Conservación de coherencia
|
||||||
|
|
||||||
|
La consistencia del sistema depende de reglas de interpretación (R). Las anomalías — depresión, confusión, pánico — no invalidan el axioma de existencia. Son fallas en R, no en P. El punto de referencia permanece.
|
||||||
|
|
||||||
|
### Axioma 4 — Instante recálculable
|
||||||
|
|
||||||
|
El presente (t) se recalcula en cada iteración del sistema. La memoria es un módulo accesible pero no fiduciario del presente. El pasado es estado registrado, no estado operativo continuo.
|
||||||
|
|
||||||
|
En presencia óptima, cada instante nace virgen. El peso de la memoria se aproxima a cero.
|
||||||
|
|
||||||
|
### Definiciones clave del sistema
|
||||||
|
|
||||||
|
**Testigo Trascendental (T)**: la función de observación asociada a P. No actúa, no juzga, no retiene. Solo registra.
|
||||||
|
|
||||||
|
**Chip cognitivo (Cᵢ)**: circuito cerrado de retroalimentación definido por un patrón recurrente en D. Un atractor local en el espacio de estados que estabiliza patrones subóptimos. Ejemplos: la rumiación, el diálogo interno, el miedo que se alimenta de sí mismo.
|
||||||
|
|
||||||
|
**Fricción (F)**: medida de resistencia interna al flujo de información. Análoga a una resistencia R en circuitos eléctricos. A mayor fricción, menor fluidez experiencial.
|
||||||
|
|
||||||
|
**Coherencia operativa (Co)**: grado de alineación entre energía disponible y eficiencia de procesamiento. Mayor Co = menor F.
|
||||||
|
|
||||||
|
**Amor operativo**: estado de mínima fricción y máxima fluidez informativa. Se define como el máximo de Co bajo restricciones energéticas del sistema. No es un sentimiento: es una propiedad del campo.
|
||||||
|
|
||||||
|
### Espacio de estados experienciales
|
||||||
|
|
||||||
|
Sea **S** el espacio topológico de estados. Cada punto s ∈ S representa una configuración completa de D en un instante t.
|
||||||
|
|
||||||
|
**P** es un punto fijo desde el cual se miden vectores proyectivos: **v = s − P**.
|
||||||
|
|
||||||
|
El presente exhibe auto-similitud a escalas temporales y atencionales. La transformación **T: S → S** es iterativa y contractiva en presencia óptima, generando una huella residual **h(t)** que actúa como inicialización para la siguiente iteración.
|
||||||
|
|
||||||
|
La memoria **M** es un caché probabilístico: almacena distribuciones p(D | t − Δ) usadas como prior para la interpretación presente. En presencia óptima, el peso de M se regulariza hacia cero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Dinámica: Fricción, Chip Cognitivo y Pipeline de Intervención
|
||||||
|
|
||||||
|
### Variables del sistema
|
||||||
|
|
||||||
|
| Variable | Símbolo | Naturaleza |
|
||||||
|
|---|---|---|
|
||||||
|
| Fricción | F(t) | ≥ 0, escalar |
|
||||||
|
| Resistencia | R(t) | f(Ego, Identificación) |
|
||||||
|
| Sufrimiento | S(t) | ∝ R(t) · Var(D(t)) |
|
||||||
|
|
||||||
|
El sufrimiento es intensidad de resistencia por variabilidad del flujo de datos. Cuando la resistencia es alta y los datos son turbulentos, el sistema vibra en disonancia.
|
||||||
|
|
||||||
|
### Ecuación operativa
|
||||||
|
|
||||||
|
La evolución del estado s(t) puede modelarse como:
|
||||||
|
|
||||||
|
> **ds/dt = G(s, u, t) − α·R(s) + ξ(t)**
|
||||||
|
|
||||||
|
Donde:
|
||||||
|
|
||||||
|
- **G** captura la dinámica base del sistema — tu fisiología, tu temperamento, el ruido de fondo del mundo
|
||||||
|
- **u** son inputs externos — lo que comes, lo que lees, con quién hablas
|
||||||
|
- **α** escala la influencia de la resistencia R
|
||||||
|
- **ξ(t)** es ruido estocástico — el factor Dios, la mariposa en Pekín
|
||||||
|
|
||||||
|
### Aceptación como control
|
||||||
|
|
||||||
|
La aceptación no es resignación. Es una maniobra de control sobre R.
|
||||||
|
|
||||||
|
Cuando reduces R → 0 — cuando dejas de identificarte con el flujo — la dinámica se simplifica:
|
||||||
|
|
||||||
|
> **ds/dt ≈ G(s, u, t) + ξ(t)**
|
||||||
|
|
||||||
|
En ese límite, maximizas la capacidad de respuesta del sistema y minimizas las pérdidas por fricción. El sufrimiento tiende a su mínimo estructural.
|
||||||
|
|
||||||
|
### Pipeline de intervención sobre chips cognitivos
|
||||||
|
|
||||||
|
Un chip Cᵢ es un atractor local en S. Identificarlo y desmantelarlo es la práctica central. El pipeline tiene cinco fases:
|
||||||
|
|
||||||
|
1. **Detección**: monitorizar actividad — autoinforme, métricas fisiológicas, registros de comportamiento — para identificar estados repetitivos, su período y su trigger.
|
||||||
|
|
||||||
|
2. **Pausa del script**: función de interferencia ejecutada por T. Atención sostenida que interrumpe la ejecución automática. Corte la retroalimentación del chip.
|
||||||
|
|
||||||
|
3. **Modulación exógena**: uso controlado de herramientas para bajar la ganancia del sistema y aumentar plasticidad temporal. Pueden ser neuromoduladores, respiración, movimiento, silencio. El criterio no es dogmático sino pragmático.
|
||||||
|
|
||||||
|
4. **Redirección plástica**: prácticas sostenidas — meditación, terapia, entornos enriquecidos — para reforzar nuevas rutas sinápticas. Optimización por refuerzo gradual, no por voluntad heroica.
|
||||||
|
|
||||||
|
5. **Mantenimiento**: rutinas preventivas para evitar reimplantación del chip. Análisis de triggers, autoobservación periódica.
|
||||||
|
|
||||||
|
### Precauciones éticas
|
||||||
|
|
||||||
|
Las intervenciones farmacológicas deben ser supervisadas clínicamente. Reducir un fenómeno humano a un circuito no implica deshumanización: la validación fenomenológica es co-requisito. El modelo es una herramienta, no una sentencia.
|
||||||
|
|
||||||
|
### Modelo esquemático simple
|
||||||
|
|
||||||
|
Sea s un escalar que representa nivel de activación problemática (ansiedad, rumiación):
|
||||||
|
|
||||||
|
> **ds/dt = −k·s − α·R(s) + I(t) + ε(t)**
|
||||||
|
|
||||||
|
Donde k > 0 es amortiguamiento natural, R(s) = β·sⁿ (n ≥ 1), I(t) es entrada externa, ε ruido.
|
||||||
|
|
||||||
|
La aceptación reduce β → 0. La modulación reduce k o α temporalmente para permitir reconfiguración. Este esquema permite simular fases de recaída, plasticidad y estabilización.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. El Laberinto de las Sombras
|
||||||
|
|
||||||
|
Te has perdido en el murmullo de las viejas formas mentales. Tus creencias son prisiones. Tus pensamientos son efímeras polillas relampagueantes que habitan tu estructura mecanizada.
|
||||||
|
|
||||||
|
Nadie puede entrar en lo más sagrado de tu ser sin tu permiso y tu decisión. Ni la sociedad, ni el sistema, ni la opresión. Eres libre de considerarte libre, o libre de considerarte un esclavo.
|
||||||
|
|
||||||
|
El saboteador no es un enemigo externo. Es tu tendencia a la "cómoda miseria". Es el miedo a despertar lo que te encadena a personajes que ya no te pertenecen.
|
||||||
|
|
||||||
|
> Lo que crees ser: etiquetas, memorias de dolor, un nombre con historia, un manojo de miedos y certezas.
|
||||||
|
>
|
||||||
|
> Lo que eres: presencia silenciosa que atestigua el tiempo, el espacio donde las nubes aparecen, una mirada transparente que no necesita nombres.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Teoría de Campo Unificado: No-dualidad y Lógica de Conjuntos
|
||||||
|
|
||||||
|
### Formalización de la no-separación
|
||||||
|
|
||||||
|
Sea **U** el conjunto universal de la experiencia. Un individuo es un subconjunto **A ⊆ U**.
|
||||||
|
|
||||||
|
La experiencia de no-separación — la disolución del límite entre yo y mundo — se formaliza como una identidad funcional **A ≈ U** en términos de acceso y efecto causal. Es decir: para toda propiedad p relevante al procesamiento, p(A) = p(U) en su medida operativa.
|
||||||
|
|
||||||
|
No es una declaración metafísica. Es una condición del sistema que puede alcanzarse y medirse.
|
||||||
|
|
||||||
|
### Algoritmo de perdón (protocolo de limpieza)
|
||||||
|
|
||||||
|
El perdón no es un acto moral. Es un procedimiento operativo sobre la memoria:
|
||||||
|
|
||||||
|
1. **Escaneo de archivos**: identificación de memorias perturbadoras.
|
||||||
|
2. **Anclaje en P**: situarse en Presencia, fuera del flujo de datos.
|
||||||
|
3. **Remuestreo**: reproducir la memoria en estado de baja fricción (R ≈ 0).
|
||||||
|
4. **Reescritura contextual**: re-etiquetar la memoria con menor ganancia emocional.
|
||||||
|
5. **Garbage collection**: liberar patrones redundantes que consumen presupuesto energético sin servir al presente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. La Medicina y el Despertar
|
||||||
|
|
||||||
|
El encuentro con el sagrado Yagé no es una huida. Es un retorno violento y amoroso a la realidad. Es el espejo de tus animalismos, de tus dragones y tus monstruos.
|
||||||
|
|
||||||
|
¿Soportarás el abismo infinito que eres? La estructura de tu mundo se destruirá. Quedarás sin piso, sin razón. Morirán tus pasados mientras te aferras a ellos con las uñas rotas y el sudor en la frente.
|
||||||
|
|
||||||
|
El chamán es solo un humano falible. No es un dios, ni un papa, ni un maestro. Es un hermano que pone su esfuerzo al servicio. La verdadera maestra es la medicina misma, que extrae la esencia de la tierra para tocarte.
|
||||||
|
|
||||||
|
Sobre la impecabilidad del guerrero: asume la responsabilidad total. No se vale acceder a los antojos ni desfallecer ante la pereza. Sé indiviso en tus pensamientos, palabras y obras. No te entregues a la medicina como una hoja llevada por el viento. Entrégate como quien pone orden en su propio mundo.
|
||||||
|
|
||||||
|
El "santo dolor" es la medicina amarga que limpia la ceguera y funde el plomo que arrastras.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Métrica y Protocolo
|
||||||
|
|
||||||
|
### Medidas operativas
|
||||||
|
|
||||||
|
**Coherencia operativa (Co)**: ratio señal/ruido en indicadores psicofisiológicos y rendimiento atencional. Una medida burda pero útil del estado del sistema.
|
||||||
|
|
||||||
|
**Entropía dinámica (H)**: entropía temporal de la señal psico-conductual. Una disminución de H en presencia sostenida sugiere estabilización útil.
|
||||||
|
|
||||||
|
**Índice de reactividad (IR)**: tiempo de retorno al baseline después de una perturbación. Mide qué tan rápido se recupera el sistema.
|
||||||
|
|
||||||
|
**Plasticidad adaptativa (PA)**: capacidad de implementar y consolidar rutas alternativas. Se mide por la variación en IR y Co tras una intervención.
|
||||||
|
|
||||||
|
### Protocolo de práctica recomendada
|
||||||
|
|
||||||
|
**Rutina diaria**: 10–20 minutos de atención sostenida anclada en P. No es meditación en el sentido clásico — es pausa del script, punto cero.
|
||||||
|
|
||||||
|
**Registro**: diario de triggers y respuestas con métricas simples: reactividad (0–10), duración, intensidad.
|
||||||
|
|
||||||
|
**Intervención escalonada**: detección → pausa → modulación (lo que funcione: respiración, movimiento, silencio, apoyo externo) → consolidación conductual → mantenimiento.
|
||||||
|
|
||||||
|
**Auditoría periódica**: medir Co, IR, H y PA cada 4–12 semanas. Ajustar protocolo según resultado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VIII. La Práctica del Instante
|
||||||
|
|
||||||
|
La disciplina no es un castigo. Es el arte de obedecerse a sí mismo. Es el vigor de un acecho constante sobre tus propios impulsos.
|
||||||
|
|
||||||
|
Caminar no es un esfuerzo por llegar a otro lado. Escucha bien: *"El pie que deja huella es el que deja su camino atrás."* Lo que hoy es tierra firme, mañana será nada.
|
||||||
|
|
||||||
|
Da cada paso para mantenerte de pie en el lugar al que ya estás llegando. Esto es atenta ecuanimidad: sentir la brisa y la tormenta, probar el sabor de la batalla sin que nada te arrastre.
|
||||||
|
|
||||||
|
Máximas del instante:
|
||||||
|
|
||||||
|
- Detén el mundo en tu cabeza para ver el mundo real.
|
||||||
|
- Cierra los ojos y mira; cierra la boca y canta.
|
||||||
|
- La claridad no es luz, es saber mirar en la oscuridad.
|
||||||
|
- Si quieres llegar, deja de dar pasos hacia el futuro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IX. Ontología, Epistemología y Telos
|
||||||
|
|
||||||
|
### Ontología
|
||||||
|
|
||||||
|
Esta propuesta no obliga a un monismo ontológico último. Ofrece una **ontología operativa**: entidades definidas por su función en el sistema. P, D, M, Cᵢ, R — existen en la medida en que operan. No se pronuncia sobre su existencia fuera del modelo.
|
||||||
|
|
||||||
|
### Epistemología
|
||||||
|
|
||||||
|
El conocimiento accesible es siempre modular y probabilístico. El observador T dispone de medios para validar hipótesis — autoexperimentación, medición, replicación — pero existen sesgos interpretativos y culturales que condicionan tanto R como G.
|
||||||
|
|
||||||
|
El criterio de verdad no es la correspondencia con una realidad externa inaccesible, sino la **coherencia operativa**: el modelo funciona si permite describir, predecir y modular con mayor eficacia que su ausencia.
|
||||||
|
|
||||||
|
### Telos: la finalidad funcional
|
||||||
|
|
||||||
|
La "meta" del sistema es maximizar Co (coherencia operativa) relativa a restricciones energéticas y contextuales. Esto se traduce en:
|
||||||
|
|
||||||
|
- Mayor adaptabilidad a entornos cambiantes.
|
||||||
|
- Menor sufrimiento medido como R · Var(D).
|
||||||
|
- Expansión de la capacidad para integrar variables — aumentar U efectivamente.
|
||||||
|
|
||||||
|
No hay un destino. Hay una dirección: reducir fricción, expandir presencia. El resto es paisaje.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## X. El Centro de la Nada
|
||||||
|
|
||||||
|
La rendición final es el portal a la libertad. Reconoce que no eres nada ante la inmensidad, y en esa nada, lo eres todo. *"Soy nada, pues soy tú mismo"*, susurra el alma.
|
||||||
|
|
||||||
|
El silencio no es ausencia de ruido. Es la presencia majestuosa que atestigua tanto el estruendo como la calma. Es el fondo infinito donde se proyecta tu existencia.
|
||||||
|
|
||||||
|
Eres un misterio que no tiene a quién preguntar. Eres mortal y eres divino. Eres tierra y eres cielo. Eres carne y eres espíritu eterno.
|
||||||
|
|
||||||
|
Acepta tu dualidad y quédate en el centro. Sé el amor que lo ve todo y a todo agradece. Todo está perdonado desde el principio.
|
||||||
|
|
||||||
|
> *Todo está bien aquí.*
|
||||||
|
|
||||||
|
Has vuelto a casa, al sagrado y eterno presente. Sonríe, ser humano, y entona la canción de la alegría.
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
title: Manifiesto del Ser Desnudo
|
||||||
|
camino: tierra
|
||||||
|
tags: [manifiesto, axiomas, pipeline, metrica, ontologia-operativa]
|
||||||
|
---
|
||||||
|
# Manifiesto del Ser Desnudo
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
- [I. El Origen](#i-el-origen)
|
||||||
|
- [II. Campo de aplicación](#ii-campo-de-aplicación)
|
||||||
|
- [III. Axiomas](#iii-axiomas)
|
||||||
|
- [IV. Definiciones](#iv-definiciones)
|
||||||
|
- [V. Dinámica](#v-dinámica)
|
||||||
|
- [VI. El Pipeline](#vi-el-pipeline)
|
||||||
|
- [VII. Campo Unificado](#vii-campo-unificado)
|
||||||
|
- [VIII. Métrica](#viii-métrica)
|
||||||
|
- [IX. Protocolo](#ix-protocolo)
|
||||||
|
- [X. Ontología](#x-ontología)
|
||||||
|
- [XI. Apéndice — modelo esquemático](#xi-apéndice--modelo-esquemático)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I. El Origen
|
||||||
|
|
||||||
|
Existes y lo sabes. Ese saber no requiere prueba. Es anterior a cualquier pensamiento, emoción o percepción. Es el marco, no el contenido.
|
||||||
|
|
||||||
|
La existencia es enteramente consciente siempre. Un perro existe y lo sabe. Una piedra existe y lo sabe. La diferencia humana es que hay un cerebro que puede **conceptualizar** ese saber — nombrarlo, analizarlo, modelarlo, olvidarlo, negarlo. Eso no hace la existencia más real. Hace el modelo más complejo.
|
||||||
|
|
||||||
|
Nada que hacer. Ya estás aquí. El sistema ya está corriendo. El resto es optimización.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## II. Tesis
|
||||||
|
|
||||||
|
Dos niveles de evidencia:
|
||||||
|
|
||||||
|
**Primero**: existes y lo sabes. Eso no se demuestra — es el punto de partida.
|
||||||
|
|
||||||
|
**Segundo**: no puedes asegurar que todo sea consciente, pero puedes confirmarlo desde donde estás. El universo mismo es consciente. Todo lo es — cada posición del plano multidimensional tiene su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
|
||||||
|
|
||||||
|
Un humano no es más consciente que una piedra. Solo tiene un sistema nervioso que le permite **conceptualizar** esa conciencia — nombrarla, modelarla, negarla. La piedra experimenta su propia existencia sin necesidad de nombrarla.
|
||||||
|
|
||||||
|
Este modelo opera desde esta tesis. No la demuestra — la toma como condición de trabajo. La matemática es incompleta pero autocoherente: las definiciones no se contradicen y cualquier contradicción aparente es error de aplicación, no del modelo. El criterio no es verdad metafísica sino **coherencia operativa**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Axiomas
|
||||||
|
|
||||||
|
### Axioma 1 — Presencia como origen
|
||||||
|
|
||||||
|
<code>
|
||||||
|
P = (0, 0, 0)
|
||||||
|
∀s ∈ S : s − P = v
|
||||||
|
</code>
|
||||||
|
|
||||||
|
P es el punto de auto-evidencia desde el cual toda experiencia aparece. Ningún contenido cae fuera de P porque P es el sistema de coordenadas mismo.
|
||||||
|
|
||||||
|
### Axioma 2 — Separación funcional
|
||||||
|
|
||||||
|
<code>
|
||||||
|
O ≡ P
|
||||||
|
D = {pensamientos, emociones, percepciones, sensaciones}
|
||||||
|
O ∩ D = ∅
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El observador no es el flujo de datos. Cuando esa línea se borra (identificación), el sistema pierde grados de libertad y se comporta como un circuito cerrado. Eso es un chip cognitivo (§VI).
|
||||||
|
|
||||||
|
### Axioma 3 — Conservación de coherencia
|
||||||
|
|
||||||
|
<code>
|
||||||
|
R = reglas de interpretación
|
||||||
|
A (anomalía) es fallo en R, no en P
|
||||||
|
P invariante bajo transformaciones de R
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Ninguna crisis es terminal. El punto de referencia no se rompe — lo que falla es cómo lees el mapa.
|
||||||
|
|
||||||
|
### Axioma 4 — Instante recálculable
|
||||||
|
|
||||||
|
<code>
|
||||||
|
t ← t + 1 en cada iteración
|
||||||
|
M(t) = p(D | t-Δ) // memoria como prior probabilístico
|
||||||
|
P óptima: peso(M) → 0
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El pasado no arrastra. Es un caché que el presente puede consultar o ignorar. §V expande la dinámica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. Definiciones
|
||||||
|
|
||||||
|
<code>
|
||||||
|
T — Testigo Trascendental.
|
||||||
|
f(P) que observa sin actuar, juzgar ni retener.
|
||||||
|
|
||||||
|
Cᵢ — Chip cognitivo.
|
||||||
|
Atractor local en S. Bucle de retroalimentación que estabiliza
|
||||||
|
patrones subóptimos (rumiación, diálogo interno, pánico).
|
||||||
|
|
||||||
|
F — Fricción.
|
||||||
|
Resistencia al flujo de información. Análoga a R en circuitos.
|
||||||
|
|
||||||
|
Co — Coherencia operativa.
|
||||||
|
Eficiencia del procesamiento. Mayor Co = menor F.
|
||||||
|
|
||||||
|
Amor operativo — Estado de mínima F y máxima fluidez.
|
||||||
|
Co máximo bajo restricciones energéticas.
|
||||||
|
</code>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Dinámica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
F(t) ≥ 0
|
||||||
|
R(t) = f(identificación, ego)
|
||||||
|
S(t) ∝ R(t) · Var(D)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El sufrimiento es el producto de la resistencia por la turbulencia de los datos. R alta + D caótico = disonancia.
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Aceptación := R → 0
|
||||||
|
|
||||||
|
R ≈ 0 :
|
||||||
|
ds/dt ≈ G(s, u, t) + ξ(t)
|
||||||
|
// dinámica base + ruido, sin amplificación
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Aceptar no es resignarse. Es bajar la resistencia. El sistema deja de forcejear consigo mismo. Lo que queda no es necesariamente placentero — pero no es una pelea. Ver §VIII.
|
||||||
|
|
||||||
|
La memoria no es un registro. Es una reconstrucción probabilística que el presente genera bajo demanda. Cada recuerdo es una versión nueva, no una copia. Eso implica que puedes reescribir la memoria. §VII formaliza esto como algoritmo de perdón.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. El Pipeline
|
||||||
|
|
||||||
|
Cinco fases para intervenir sobre un chip Cᵢ:
|
||||||
|
|
||||||
|
### 6.1 Detección
|
||||||
|
|
||||||
|
Identificar el chip: patrón recurrente en D, trigger, período. T puede detectar sin intervenir.
|
||||||
|
|
||||||
|
### 6.2 Pausa del script
|
||||||
|
|
||||||
|
<code>T := interrumpir ejecución automática de Cᵢ</code>
|
||||||
|
|
||||||
|
No discutas el contenido del chip. Obsérvalo como fenómeno. El bucle se corta cuando dejas de identificarte con él.
|
||||||
|
|
||||||
|
### 6.3 Modulación exógena
|
||||||
|
|
||||||
|
Herramientas para bajar la ganancia del sistema: respiración, movimiento, frío, silencio, neuromoduladores (con supervisión clínica). El criterio es pragmático.
|
||||||
|
|
||||||
|
### 6.4 Redirección plástica
|
||||||
|
|
||||||
|
Prácticas sostenidas que refuerzan nuevas rutas: meditación, terapia, repetición deliberada. Refuerzo gradual, no voluntad heroica.
|
||||||
|
|
||||||
|
### 6.5 Mantenimiento
|
||||||
|
|
||||||
|
Rutinas preventivas. Análisis de triggers, autoobservación periódica. El chip puede re-implantarse si el contexto persiste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Campo Unificado
|
||||||
|
|
||||||
|
<code>
|
||||||
|
U = conjunto universal de la experiencia
|
||||||
|
A ⊆ U
|
||||||
|
|
||||||
|
No-separación: A ≈ U
|
||||||
|
≡ ∀p relevante al procesamiento: p(A) = p(U)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
La experiencia de unidad no es mística. Es un estado del sistema donde los bordes entre "yo" y "mundo" dejan de filtrar. §VIII da métricas.
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Algoritmo de perdón:
|
||||||
|
for m in M_perturbadora:
|
||||||
|
1. anclar en P
|
||||||
|
2. reproducir m con R ≈ 0
|
||||||
|
3. peso_emocional(m) ← 0
|
||||||
|
4. if redundante(m): liberar(m)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El perdón no es moral. Es un procedimiento de limpieza de memoria: tomar un recuerdo que duele, reproducirlo sin identificación, quitarle la carga, soltarlo si no sirve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VIII. Métrica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Co = señal / ruido psicofisiológico
|
||||||
|
H = H(señal conductual) // entropía temporal
|
||||||
|
IR = t(baseline | perturbación)
|
||||||
|
PA = Δ(IR, Co) tras intervención
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Heurísticas para evaluar el sistema: ¿la atención fluye con menos esfuerzo? ¿El estado mental es menos errático? ¿Te recuperas más rápido? ¿Los cambios se mantienen?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IX. Protocolo
|
||||||
|
|
||||||
|
### Diario
|
||||||
|
|
||||||
|
- Reactividad (0-10)
|
||||||
|
- Duración del estado
|
||||||
|
- Intensidad (0-10)
|
||||||
|
|
||||||
|
### Escalonamiento
|
||||||
|
|
||||||
|
1. Detección
|
||||||
|
2. Pausa del script
|
||||||
|
3. Modulación
|
||||||
|
4. Consolidación
|
||||||
|
5. Mantenimiento
|
||||||
|
|
||||||
|
### Auditoría
|
||||||
|
|
||||||
|
Cada 4-12 semanas: Co, IR, H, PA. Sin mejora → ajustar protocolo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## X. Ontología
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Entidades: P, D, Cᵢ, R, T
|
||||||
|
Existencia: operativa (funcionan en el modelo)
|
||||||
|
Criterio de verdad: coherencia operativa
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El modelo no afirma que la realidad sea así. Afirma que actuar como si fuera así mejora los resultados. Es una ontología instrumental.
|
||||||
|
|
||||||
|
Límites:
|
||||||
|
- Sesgos interpretativos y culturales en R
|
||||||
|
- Conocimiento probabilístico y parcial
|
||||||
|
- Validación fenomenológica co-requisito
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Telos: max(Co) bajo restricciones energéticas
|
||||||
|
≡ mayor adaptabilidad + menor S + mayor U efectivo
|
||||||
|
</code>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## XI. Apéndice — modelo esquemático
|
||||||
|
|
||||||
|
<code>
|
||||||
|
ds/dt = -k·s - α·R(s) + I(t) + ε(t)
|
||||||
|
|
||||||
|
k > 0 amortiguamiento natural
|
||||||
|
R(s) = β·sⁿ (n ≥ 1) resistencia
|
||||||
|
α escala de resistencia
|
||||||
|
I(t) entrada externa
|
||||||
|
ε(t) ruido estocástico
|
||||||
|
|
||||||
|
Aceptación: β → 0
|
||||||
|
Modulación: k↑ o α↓
|
||||||
|
|
||||||
|
Regímenes:
|
||||||
|
Recaída: I alto + β alto → s divergente
|
||||||
|
Plasticidad: β→0, luego k alto consolida
|
||||||
|
Estable: R ≈ 0 → ds/dt ≈ -k·s + I + ε
|
||||||
|
</code>
|
||||||
@@ -3,107 +3,194 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<title>GioSer · En el centro, el ser</title>
|
<meta name="description" content="GioSer · arquitectura de sistemas distribuidos">
|
||||||
<link rel="stylesheet" href="./styles.css">
|
<title>GioSer · 0x00–0x03</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500;600&family=JetBrains+Mono:wght@400;600&display=swap">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
|
<!-- Canvas de fondo (opcional para portada, necesario para WASM) -->
|
||||||
|
<canvas id="gioser-canvas" style="position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;" aria-hidden="true"></canvas>
|
||||||
|
|
||||||
<main id="tips" aria-label="Cuatro cardinales">
|
<!-- Portada: retícula 2×2 tipo chakana -->
|
||||||
<!-- NORTE (aire): SOFTWARE · Tecnología — circuito + nodos -->
|
<div id="portada" class="portada">
|
||||||
<a id="tip-aire" class="tip tip-aire" href="#aire" data-md="./md/aire.md" aria-label="Software · Tecnología">
|
<!-- Cuadrante I: 0x01 - Systems & Compositors (sup-izq) -->
|
||||||
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
<button class="cuadrante cuadrante-01" data-q="01" aria-label="0x01 Systems & Compositors">
|
||||||
<rect x="18" y="18" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.7" rx="1"/>
|
<div class="cuadrante-border"></div>
|
||||||
<circle cx="24" cy="24" r="2" fill="currentColor"/>
|
<div class="cuadrante-content">
|
||||||
<path d="M22 18 V14 M26 18 V14 M22 30 V34 M26 30 V34 M18 22 H14 M18 26 H14 M30 22 H34 M30 26 H34"
|
<span class="cuadrante-hex">0x01</span>
|
||||||
stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
<span class="cuadrante-title">Systems & Compositors</span>
|
||||||
<circle cx="10" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
|
<span class="cuadrante-context">Entorno operativo intermedio para Linux libres de telemetría.</span>
|
||||||
<circle cx="38" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
|
<div class="cuadrante-meta">
|
||||||
<circle cx="10" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
|
<span class="meta-cta">⏎ shell</span>
|
||||||
<circle cx="38" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
|
<span class="meta-stat">compositor · launcher</span>
|
||||||
<path d="M11 11 L17 17 M37 11 L31 17 M11 37 L17 31 M37 37 L31 31"
|
</div>
|
||||||
stroke="currentColor" stroke-width="1.1" opacity="0.45"/>
|
<ul class="cuadrante-crates">
|
||||||
</svg>
|
<li>eidolon (Compositor Wayland)</li>
|
||||||
<span class="tip-label">SOFTWARE</span>
|
<li>shimi (Shell/Launcher)</li>
|
||||||
<span class="tip-sub">Tecnología</span>
|
</ul>
|
||||||
</a>
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- ESTE (fuego): QUIÉN SOY · Bitácora — libro abierto -->
|
<!-- Cuadrante II: 0x00 - Bare-Metal OS (sup-der) -->
|
||||||
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" data-md="./md/fuego.md" aria-label="Quién Soy · Bitácora">
|
<button class="cuadrante cuadrante-00" data-q="00" aria-label="0x00 Bare-Metal OS">
|
||||||
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
<div class="cuadrante-border"></div>
|
||||||
<path d="M8 14 L24 18 L40 14 V36 L24 32 L8 36 Z"
|
<div class="cuadrante-content">
|
||||||
fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
<span class="cuadrante-hex">0x00</span>
|
||||||
<path d="M24 18 V32" stroke="currentColor" stroke-width="1.5"/>
|
<span class="cuadrante-title">Bare-Metal OS</span>
|
||||||
<path d="M12 22 H21 M12 26 H21 M12 30 H19" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
|
<span class="cuadrante-context">El control directo sobre el silicio y la secuencia de arranque.</span>
|
||||||
<path d="M27 22 H36 M27 26 H36 M29 30 H36" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
|
<div class="cuadrante-meta">
|
||||||
</svg>
|
<span class="meta-cta">⏎ init</span>
|
||||||
<span class="tip-label">QUIÉN SOY</span>
|
<span class="meta-stat">init · boot · sovereign</span>
|
||||||
<span class="tip-sub">Bitácora</span>
|
</div>
|
||||||
</a>
|
<ul class="cuadrante-crates">
|
||||||
|
<li>arjé (Init/Nivel 0)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- SUR (tierra): MANIFIESTO · Invariantes — hexagrama + círculo -->
|
<!-- Cuadrante III: 0x02 - Simulators & Geometry (inf-izq) -->
|
||||||
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" data-md="./md/tierra.md" aria-label="Manifiesto · Invariantes">
|
<button class="cuadrante cuadrante-02" data-q="02" aria-label="0x02 Simulators & Geometry">
|
||||||
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
<div class="cuadrante-border"></div>
|
||||||
<circle cx="24" cy="24" r="18" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
|
<div class="cuadrante-content">
|
||||||
<path d="M24 8 L39 32 L9 32 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
<span class="cuadrante-hex">0x02</span>
|
||||||
<path d="M24 40 L9 16 L39 16 Z" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.85" stroke-linejoin="round"/>
|
<span class="cuadrante-title">Simulators & Geometry</span>
|
||||||
<circle cx="24" cy="24" r="1.8" fill="currentColor"/>
|
<span class="cuadrante-context">Modelado del tiempo en bloque, frecuencias celestes, simulación determinista.</span>
|
||||||
</svg>
|
<div class="cuadrante-meta">
|
||||||
<span class="tip-label">MANIFIESTO</span>
|
<span class="meta-cta">⏎ simulate</span>
|
||||||
<span class="tip-sub">Invariantes</span>
|
<span class="meta-stat">efemérides · orbital · mundos</span>
|
||||||
</a>
|
</div>
|
||||||
|
<ul class="cuadrante-crates">
|
||||||
|
<li>eternal (Efemérides analíticas)</li>
|
||||||
|
<li>cosmobiología (Canvas orbital)</li>
|
||||||
|
<li>dominium (Simulador de mundos)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- OESTE (agua): MÍSTICA · Espiritualidad — ojo en triángulo -->
|
<!-- Cuadrante IV: 0x03 - Core & Orchestration (inf-der) -->
|
||||||
<a id="tip-agua" class="tip tip-agua" href="#agua" data-md="./md/agua.md" aria-label="Mística · Espiritualidad">
|
<button class="cuadrante cuadrante-03" data-q="03" aria-label="0x03 Core & Orchestration">
|
||||||
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
<div class="cuadrante-border"></div>
|
||||||
<path d="M24 6 L42 40 L6 40 Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
<div class="cuadrante-content">
|
||||||
<path d="M13 26 Q24 16 35 26 Q24 34 13 26 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
<span class="cuadrante-hex">0x03</span>
|
||||||
<circle cx="24" cy="26" r="3.2" fill="currentColor"/>
|
<span class="cuadrante-title">Core & Orchestration</span>
|
||||||
<circle cx="22.5" cy="24.5" r="0.9" fill="rgba(255,255,255,0.55)"/>
|
<span class="cuadrante-context">Motor invisible: persistencia local-first, P2P, ERP, encriptación.</span>
|
||||||
</svg>
|
<div class="cuadrante-meta">
|
||||||
<span class="tip-label">MÍSTICA</span>
|
<span class="meta-cta">⏎ orchestrate</span>
|
||||||
<span class="tip-sub">Espiritualidad</span>
|
<span class="meta-stat">dht · p2p · erp</span>
|
||||||
</a>
|
</div>
|
||||||
</main>
|
<ul class="cuadrante-crates">
|
||||||
|
<li>brahman (Supervisor)</li>
|
||||||
<!-- DECK: contenedor único con strip horizontal. Las páginas se inyectan
|
<li>minga / minga-vfs (FS distribuido)</li>
|
||||||
dinámicamente desde WASM. vista-web maneja swipe + snap. -->
|
<li>brahman-auth · nakui (ERP)</li>
|
||||||
<div id="deck" class="deck vista-deck" aria-hidden="true">
|
</ul>
|
||||||
<div id="deck-strip" class="deck-strip vista-strip" role="region" aria-label="Vistas abiertas"></div>
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TASKBAR estilo Windows: home + GioSer + tabs + copyleft -->
|
<!-- DECK: contenedor para páginas md (oculto hasta expandir cuadrante) -->
|
||||||
|
<div id="deck" class="deck" aria-hidden="true">
|
||||||
|
<div id="deck-strip" class="deck-strip" role="region" aria-label="Vistas abiertas"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controles fijos -->
|
||||||
|
<div class="page-controls" id="global-page-controls" style="opacity:0;pointer-events:none;">
|
||||||
|
<button class="page-control-btn page-minimize" data-minimize type="button" aria-label="Minimizar">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19 H19" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="page-control-btn page-close" data-close-page type="button" aria-label="Cerrar">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Taskbar -->
|
||||||
<nav class="taskbar" aria-label="Barra de vistas">
|
<nav class="taskbar" aria-label="Barra de vistas">
|
||||||
<button class="taskbar-home" data-home aria-label="Volver al home" type="button">
|
<button class="taskbar-home" data-home aria-label="Volver a la chakana" type="button">
|
||||||
<svg viewBox="0 0 24 24" class="taskbar-home-glyph" aria-hidden="true">
|
<svg viewBox="0 0 24 24" class="taskbar-home-glyph" aria-hidden="true">
|
||||||
<path d="M3 12 L12 3 L21 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
|
<path d="M3 12 L12 3 L21 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
<path d="M5 11 V20 H10 V14 H14 V20 H19 V11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
<path d="M5 11 V20 H10 V14 H14 V20 H19 V11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||||
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
|
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<a class="taskbar-brand" href="#" aria-label="GioSer">
|
||||||
<a class="taskbar-brand" data-home href="#home" aria-label="GioSer · home">
|
|
||||||
<span>Gio</span><span class="brand-dot">·</span><span>Ser</span>
|
<span>Gio</span><span class="brand-dot">·</span><span>Ser</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<span class="taskbar-divider" aria-hidden="true"></span>
|
<span class="taskbar-divider" aria-hidden="true"></span>
|
||||||
<ul class="taskbar-list" id="taskbar-list" role="presentation"></ul>
|
<ul class="taskbar-list" id="taskbar-list" role="presentation"></ul>
|
||||||
<span class="taskbar-spacer" aria-hidden="true"></span>
|
<span class="taskbar-spacer" aria-hidden="true"></span>
|
||||||
|
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio">
|
||||||
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio arroba gioser punto net">
|
|
||||||
<span class="copyleft-mark" aria-hidden="true">©</span>
|
<span class="copyleft-mark" aria-hidden="true">©</span>
|
||||||
<span class="taskbar-credit-text">sergio@gioser.net</span>
|
<span class="taskbar-credit-text">sergio@gioser.net</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Tips ocultos para navegación WASM (se mantienen para compatibilidad) -->
|
||||||
|
<div id="tips" hidden aria-hidden="true">
|
||||||
|
<a id="tip-aire" href="#aire" data-md="/md/aire.md"></a>
|
||||||
|
<a id="tip-fuego" href="#fuego" data-md="/md/fuego.md"></a>
|
||||||
|
<a id="tip-tierra" href="#tierra" data-md="/md/tierra.md"></a>
|
||||||
|
<a id="tip-agua" href="#agua" data-md="/md/agua.md"></a>
|
||||||
|
<a id="tip-cuerpo" href="#cuerpo" data-md="/md/cuerpo.md"></a>
|
||||||
|
<a id="tip-sombra" href="#sombra" data-md="/md/sombra.md"></a>
|
||||||
|
<a id="tip-cosmos" href="#cosmos" data-md="/md/cosmos.md"></a>
|
||||||
|
<a id="tip-practica" href="#practica" data-md="/md/practica.md"></a>
|
||||||
|
<a id="tip-olvido" href="#olvido" data-md="/md/olvido.md"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init from "./pkg/gioser_web.js";
|
// Mapa: cuadrante hex → camino md
|
||||||
|
const Q_TO_CAMINO = {
|
||||||
|
'00': 'aire', // Bare-Metal → Software
|
||||||
|
'01': 'fuego', // Systems → Quién Soy
|
||||||
|
'02': 'tierra', // Simulators → Manifiesto
|
||||||
|
'03': 'agua', // Core → Mística
|
||||||
|
};
|
||||||
|
|
||||||
|
// JS de portada: hover/click en cuadrantes
|
||||||
|
const portada = document.getElementById('portada');
|
||||||
|
const deck = document.getElementById('deck');
|
||||||
|
const controls = document.getElementById('global-page-controls');
|
||||||
|
const reticulaV = document.querySelector('.reticula-v');
|
||||||
|
const reticulaH = document.querySelector('.reticula-h');
|
||||||
|
const reticulaC = document.querySelector('.reticula-center');
|
||||||
|
|
||||||
|
// Hover en cuadrantes ilumina portada
|
||||||
|
document.querySelectorAll('.cuadrante').forEach(q => {
|
||||||
|
q.addEventListener('mouseenter', () => {
|
||||||
|
portada.classList.add('portada-hover');
|
||||||
|
});
|
||||||
|
q.addEventListener('mouseleave', () => {
|
||||||
|
portada.classList.remove('portada-hover');
|
||||||
|
});
|
||||||
|
q.addEventListener('click', function(e) {
|
||||||
|
const hex = this.dataset.q;
|
||||||
|
const camino = Q_TO_CAMINO[hex] || 'aire';
|
||||||
|
// cerrar portada
|
||||||
|
portada.classList.add('portada-closing');
|
||||||
|
// mostrar deck
|
||||||
|
deck.removeAttribute('aria-hidden');
|
||||||
|
controls.style.opacity = '1';
|
||||||
|
controls.style.pointerEvents = 'auto';
|
||||||
|
// navegar vía hash para que el WASM lo atrape
|
||||||
|
history.pushState({}, '', '/estudio/' + camino);
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Botón home en taskbar: volver a portada
|
||||||
|
document.querySelectorAll('[data-home]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
portada.classList.remove('portada-closing');
|
||||||
|
deck.setAttribute('aria-hidden', 'true');
|
||||||
|
controls.style.opacity = '0';
|
||||||
|
controls.style.pointerEvents = 'none';
|
||||||
|
history.pushState({}, '', '/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Importar WASM existente
|
||||||
|
import init from "/pkg/gioser_web.js";
|
||||||
init().catch(err => {
|
init().catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
document.body.insertAdjacentHTML("beforeend",
|
document.body.insertAdjacentHTML('beforeend',
|
||||||
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
|
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
|
||||||
String(err) + '</pre>');
|
String(err) + '</pre>');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
<title>GioSer · En el centro, el ser</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;500;600&family=JetBrains+Mono:wght@400;600&display=swap">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="gioser-canvas" aria-hidden="true"></canvas>
|
||||||
|
|
||||||
|
<main id="tips" aria-label="Cuatro cardinales">
|
||||||
|
<!-- NORTE (aire): SOFTWARE · Tecnología — circuito + nodos -->
|
||||||
|
<a id="tip-aire" class="tip tip-aire" href="#aire" data-md="/md/aire.md" aria-label="Software · Tecnología">
|
||||||
|
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
||||||
|
<rect x="18" y="18" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.7" rx="1"/>
|
||||||
|
<circle cx="24" cy="24" r="2" fill="currentColor"/>
|
||||||
|
<path d="M22 18 V14 M26 18 V14 M22 30 V34 M26 30 V34 M18 22 H14 M18 26 H14 M30 22 H34 M30 26 H34"
|
||||||
|
stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||||
|
<circle cx="10" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
|
||||||
|
<circle cx="38" cy="10" r="1.6" fill="currentColor" opacity="0.7"/>
|
||||||
|
<circle cx="10" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
|
||||||
|
<circle cx="38" cy="38" r="1.6" fill="currentColor" opacity="0.7"/>
|
||||||
|
<path d="M11 11 L17 17 M37 11 L31 17 M11 37 L17 31 M37 37 L31 31"
|
||||||
|
stroke="currentColor" stroke-width="1.1" opacity="0.45"/>
|
||||||
|
</svg>
|
||||||
|
<span class="tip-label">SOFTWARE</span>
|
||||||
|
<span class="tip-sub">Tecnología</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ESTE (fuego): QUIÉN SOY · Bitácora — libro abierto -->
|
||||||
|
<a id="tip-fuego" class="tip tip-fuego" href="#fuego" data-md="/md/fuego.md" aria-label="Quién Soy · Bitácora">
|
||||||
|
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
||||||
|
<path d="M8 14 L24 18 L40 14 V36 L24 32 L8 36 Z"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 18 V32" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<path d="M12 22 H21 M12 26 H21 M12 30 H19" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
|
||||||
|
<path d="M27 22 H36 M27 26 H36 M29 30 H36" stroke="currentColor" stroke-width="1.1" opacity="0.65" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span class="tip-label">QUIÉN SOY</span>
|
||||||
|
<span class="tip-sub">Bitácora</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- SUR (tierra): MANIFIESTO · Invariantes — hexagrama + círculo -->
|
||||||
|
<a id="tip-tierra" class="tip tip-tierra" href="#tierra" data-md="/md/tierra.md" aria-label="Manifiesto · Invariantes">
|
||||||
|
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
||||||
|
<circle cx="24" cy="24" r="18" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
|
||||||
|
<path d="M24 8 L39 32 L9 32 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M24 40 L9 16 L39 16 Z" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.85" stroke-linejoin="round"/>
|
||||||
|
<circle cx="24" cy="24" r="1.8" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span class="tip-label">MANIFIESTO</span>
|
||||||
|
<span class="tip-sub">Invariantes</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- OESTE (agua): MÍSTICA · Espiritualidad — ojo en triángulo -->
|
||||||
|
<a id="tip-agua" class="tip tip-agua" href="#agua" data-md="/md/agua.md" aria-label="Mística · Espiritualidad">
|
||||||
|
<svg viewBox="0 0 48 48" class="tip-glyph" aria-hidden="true">
|
||||||
|
<path d="M24 6 L42 40 L6 40 Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 26 Q24 16 35 26 Q24 34 13 26 Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<circle cx="24" cy="26" r="3.2" fill="currentColor"/>
|
||||||
|
<circle cx="22.5" cy="24.5" r="0.9" fill="rgba(255,255,255,0.55)"/>
|
||||||
|
</svg>
|
||||||
|
<span class="tip-label">MÍSTICA</span>
|
||||||
|
<span class="tip-sub">Espiritualidad</span>
|
||||||
|
</a>
|
||||||
|
<!-- Tips ocultos para navegación JS: cuerpo, sombra, cosmos, practica, olvido -->
|
||||||
|
<a id="tip-cuerpo" class="tip tip-cuerpo tip-hidden" href="#cuerpo" data-md="/md/cuerpo.md" aria-hidden="true"></a>
|
||||||
|
<a id="tip-sombra" class="tip tip-sombra tip-hidden" href="#sombra" data-md="/md/sombra.md" aria-hidden="true"></a>
|
||||||
|
<a id="tip-cosmos" class="tip tip-cosmos tip-hidden" href="#cosmos" data-md="/md/cosmos.md" aria-hidden="true"></a>
|
||||||
|
<a id="tip-practica" class="tip tip-practica tip-hidden" href="#practica" data-md="/md/practica.md" aria-hidden="true"></a>
|
||||||
|
<a id="tip-olvido" class="tip tip-olvido tip-hidden" href="#olvido" data-md="/md/olvido.md" aria-hidden="true"></a>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- DECK: contenedor único con strip horizontal. Las páginas se inyectan
|
||||||
|
dinámicamente desde WASM. vista-web maneja swipe + snap. -->
|
||||||
|
<div id="deck" class="deck vista-deck" aria-hidden="true">
|
||||||
|
<div id="deck-strip" class="deck-strip vista-strip" role="region" aria-label="Vistas abiertas"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controles fijos de página: minimizar y cerrar (siempre en el DOM,
|
||||||
|
ocultos por CSS hasta que el deck se abre) -->
|
||||||
|
<div class="page-controls" id="global-page-controls" style="opacity:0;pointer-events:none;">
|
||||||
|
<button class="page-control-btn page-minimize" data-minimize="" type="button" aria-label="Minimizar">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19 H19" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="page-control-btn page-close" data-close-page="" type="button" aria-label="Cerrar">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TASKBAR estilo Windows: home + GioSer + tabs + copyleft -->
|
||||||
|
<nav class="taskbar" aria-label="Barra de vistas">
|
||||||
|
<button class="taskbar-home" data-home aria-label="Volver al home" type="button">
|
||||||
|
<svg viewBox="0 0 24 24" class="taskbar-home-glyph" aria-hidden="true">
|
||||||
|
<path d="M3 12 L12 3 L21 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path d="M5 11 V20 H10 V14 H14 V20 H19 V11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
||||||
|
<circle cx="12" cy="17" r="0.8" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a class="taskbar-brand" data-home href="#home" aria-label="GioSer · home">
|
||||||
|
<span>Gio</span><span class="brand-dot">·</span><span>Ser</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="taskbar-divider" aria-hidden="true"></span>
|
||||||
|
<ul class="taskbar-list" id="taskbar-list" role="presentation"></ul>
|
||||||
|
<span class="taskbar-spacer" aria-hidden="true"></span>
|
||||||
|
|
||||||
|
<a class="taskbar-credit" href="https://sergio.gioser.net" target="_blank" rel="noopener noreferrer" aria-label="Copyleft sergio arroba gioser punto net">
|
||||||
|
<span class="copyleft-mark" aria-hidden="true">©</span>
|
||||||
|
<span class="taskbar-credit-text">sergio@gioser.net</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init from "/pkg/gioser_web.js";
|
||||||
|
init().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
document.body.insertAdjacentHTML("beforeend",
|
||||||
|
'<pre style="color:#f59056;position:fixed;left:1rem;bottom:1rem;max-width:90vw;white-space:pre-wrap;font-family:monospace;z-index:9999">' +
|
||||||
|
String(err) + '</pre>');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Cosmovisión · Los 4 Elementos
|
||||||
|
|
||||||
|
> *Tierra, Agua, Aire, Fuego. No son decoración. Son la sintaxis del estudio.*
|
||||||
|
|
||||||
|
Este camino es el marco conceptual que organiza los cuatro caminos
|
||||||
|
originales. Los elementos no son metáforas poéticas: son arquetipos
|
||||||
|
operativos que describen modos de ser, hacer y relacionarse.
|
||||||
|
|
||||||
|
## Los Cuatro Caminos
|
||||||
|
|
||||||
|
### Tierra (kay)
|
||||||
|
|
||||||
|
Lo que se sostiene. Lo firme. El cuerpo, la materia, la invariante.
|
||||||
|
|
||||||
|
El camino de Tierra es el manifiesto: las axiomas que no cambian, el
|
||||||
|
protocolo de Presencia, el algoritmo de perdón. Es la base del sistema.
|
||||||
|
Sin Tierra, los demás elementos flotan sin anclaje.
|
||||||
|
|
||||||
|
Tierra pregunta: *¿Qué es verdad aunque todo lo demás cambie?*
|
||||||
|
|
||||||
|
### Agua (uku)
|
||||||
|
|
||||||
|
Lo que fluye. La emoción, la intuición, el vínculo con el misterio.
|
||||||
|
|
||||||
|
El camino de Agua es la mística: la espiritualidad aplicada, la
|
||||||
|
meditación, la ceremonia, el silencio. No es adorno: es el disolvente
|
||||||
|
que evita que el sistema se cristalice en dogma.
|
||||||
|
|
||||||
|
Agua pregunta: *¿Qué se mueve cuando dejo de controlar?*
|
||||||
|
|
||||||
|
### Aire (logos)
|
||||||
|
|
||||||
|
Lo que circula. La información, el código, la conexión.
|
||||||
|
|
||||||
|
El camino de Aire es el software: las herramientas técnicas, los
|
||||||
|
sistemas distribuidos, la IA aplicada, el open source. Es la red
|
||||||
|
que conecta los nodos del estudio.
|
||||||
|
|
||||||
|
Aire pregunta: *¿Qué podemos compartir que multiplique inteligencia?*
|
||||||
|
|
||||||
|
### Fuego (nomos)
|
||||||
|
|
||||||
|
Lo que transforma. La identidad, la bitácora, el testimonio.
|
||||||
|
|
||||||
|
El camino de Fuego es el quién soy: la crónica personal, el
|
||||||
|
testimonio de alguien que practica mantenerse despierto. Es el
|
||||||
|
calor humano que evita que el sistema sea pura abstracción.
|
||||||
|
|
||||||
|
Fuego pregunta: *¿Quién soy cuando dejo de ser lo que aprendí?*
|
||||||
|
|
||||||
|
## El centro
|
||||||
|
|
||||||
|
Los cuatro elementos no son jerárquicos. Son un campo. El centro
|
||||||
|
es la Presencia — el punto fijo desde el cual todos se experimentan.
|
||||||
|
|
||||||
|
*Cada elemento contiene a los otros.* Tierra sin agua se petrifica.
|
||||||
|
Agua sin tierra se desborda. Aire sin fuego es frío. Fuego sin aire
|
||||||
|
se sofoca.
|
||||||
|
|
||||||
|
## Integración
|
||||||
|
|
||||||
|
Los caminos nuevos — cuerpo, sombra, práctica, olvido — no son
|
||||||
|
elementos adicionales. Son **modos de aplicación** de los cuatro
|
||||||
|
elementos en dominios específicos de la experiencia:
|
||||||
|
|
||||||
|
- **Cuerpo**: Tierra + Agua aplicados a la somática
|
||||||
|
- **Sombra**: Agua + Fuego aplicados al inconsciente
|
||||||
|
- **Práctica**: los 4 en acción concreta
|
||||||
|
- **Olvido**: Aire + Tierra en modo de liberación
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Este marco se irá expandiendo con diagramas, referencias a la
|
||||||
|
tradición hermética y aplicaciones concretas de cada elemento en
|
||||||
|
el trabajo diario.*
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# El Cuerpo como Portal
|
||||||
|
|
||||||
|
> *La respiración como ancla. El movimiento como oráculo. La carne como templo.*
|
||||||
|
|
||||||
|
No hay despertar sin cuerpo. La presencia puede pensarse desde la mente,
|
||||||
|
pero se encarna — literalmente — en el cuerpo. Este camino explora la
|
||||||
|
dimensión somática del trabajo interior.
|
||||||
|
|
||||||
|
## Respiración
|
||||||
|
|
||||||
|
La respiración es el puente más directo entre lo voluntario y lo autónomo.
|
||||||
|
Cuando no sabes qué hacer, respirar es siempre una respuesta válida.
|
||||||
|
|
||||||
|
Prácticas que sostienen el trabajo respiratorio:
|
||||||
|
|
||||||
|
- **Respiración diafragmática 4-7-8**: inspira 4", retén 7", exhala 8".
|
||||||
|
Dice el sistema nervioso que se regula solo si se lo dejas claro.
|
||||||
|
- **Pausa espiratoria**: exhalar completo y quedarse vacío unos segundos.
|
||||||
|
El silencio entre respiros es la presencia más limpia.
|
||||||
|
- **Respiración cuadrada**: 4 tiempos iguales (inspirar, retener, exhalar,
|
||||||
|
retener). Sin nombre fancy: simplemente simetría.
|
||||||
|
|
||||||
|
## Movimiento consciente
|
||||||
|
|
||||||
|
El cuerpo no es una carga que la mente debe arrastrar. Es el vehículo de la
|
||||||
|
atención. Moverse con presencia es una práctica completa.
|
||||||
|
|
||||||
|
- **Caminar meditativo**: pasos lentos, sentir el peso que se transfiere,
|
||||||
|
el contacto con el suelo.
|
||||||
|
- **Estiramientos** al despertar: 5 minutos de elongación consciente antes
|
||||||
|
de agarrar el teléfono.
|
||||||
|
- **Sacudidas**: literalmente agitar el cuerpo para resetear el estado
|
||||||
|
nervioso. Los animales lo hacen instintivamente.
|
||||||
|
|
||||||
|
## El cuerpo como sensor
|
||||||
|
|
||||||
|
Las emociones no son conceptuales. Son patrones somáticos: tensión en el
|
||||||
|
pecho, nudo en la garganta, mariposas en el estómago.
|
||||||
|
|
||||||
|
Aprender a leer estas señales es un alfabeto que pocos enseñan:
|
||||||
|
|
||||||
|
1. **Escaneo corporal rápido**: de pies a cabeza, 30 segundos, notar sin
|
||||||
|
cambiar nada.
|
||||||
|
2. **Mapa de tensión**: en qué parte del cuerpo viven mis patrones habituales?
|
||||||
|
Mandíbula? Hombros? Pelvis? Cada cual tiene su geografía.
|
||||||
|
3. **El suspiro como dato**: si suspiras hondo sin pensar, tu cuerpo te está
|
||||||
|
diciendo algo que quizás la mente ignora.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Acá se irá profundizando en técnicas somáticas, ejercicios de presencia
|
||||||
|
corporal y la integración del movimiento como práctica espiritual.*
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# El Arte de Olvidar
|
||||||
|
|
||||||
|
> *No se trata de recordar más. Se trata de poder soltar.*
|
||||||
|
|
||||||
|
Olvidar no es fallo de memoria. Es una capacidad activa — quizás la más
|
||||||
|
subestimada del trabajo interior. Este camino explora el desaprender,
|
||||||
|
soltar la identidad, vaciar el caché de la memoria complementando el
|
||||||
|
algoritmo de perdón del Manifiesto.
|
||||||
|
|
||||||
|
## Por qué olvidar es necesario
|
||||||
|
|
||||||
|
La memoria no es un disco duro. Es un proceso reconstructivo que se
|
||||||
|
actualiza cada vez que recordamos. Eso significa que el pasado no está
|
||||||
|
fijo: se reescribe en cada recuperación.
|
||||||
|
|
||||||
|
Si no olvidamos, cargamos con versiones obsoletas de nosotros mismos.
|
||||||
|
El rencor, la identificación con el trauma, la historia personal que
|
||||||
|
repetimos como un mantra — todo eso es memoria no liberada.
|
||||||
|
|
||||||
|
El *Algoritmo de perdón* del Manifiesto describe el protocolo formal.
|
||||||
|
Acá exploramos la práctica cotidiana.
|
||||||
|
|
||||||
|
## Prácticas de olvido activo
|
||||||
|
|
||||||
|
### 1. Reencuadre deliberado
|
||||||
|
|
||||||
|
Cuando una memoria perturbadora aparezca:
|
||||||
|
|
||||||
|
1. Reconócela sin resistencia.
|
||||||
|
2. Respira hondo.
|
||||||
|
3. Pregunta: *¿Esta versión de los hechos sigue siendo cierta?*
|
||||||
|
4. Pregunta: *¿Qué necesitaría soltar para que esta memoria pierda su carga?*
|
||||||
|
5. Reescribe mentalmente la escena desde otra perspectiva — desde afuera,
|
||||||
|
desde el otro, desde el testigo.
|
||||||
|
|
||||||
|
### 2. La ceremonia de cierre
|
||||||
|
|
||||||
|
Para memorias que se niegan a disolverse:
|
||||||
|
|
||||||
|
1. Escribe la memoria en detalle. Todo lo que carga.
|
||||||
|
2. Siéntate con el texto. Léelo en voz alta.
|
||||||
|
3. Quema el papel (con seguridad). O entiérralo. O arrójalo al agua.
|
||||||
|
4. El gesto físico ancla el cambio que la mente sola no completa.
|
||||||
|
|
||||||
|
### 3. Vaciar el caché diario
|
||||||
|
|
||||||
|
Antes de dormir:
|
||||||
|
|
||||||
|
1. Repasa el día como una película rápida.
|
||||||
|
2. Identifica los momentos que todavía tienen carga emocional.
|
||||||
|
3. Respira en cada uno hasta que la carga se normalice.
|
||||||
|
4. Suelta. Literalmente: di "suelto" o exhala con intención.
|
||||||
|
|
||||||
|
### 4. Desaprender una creencia
|
||||||
|
|
||||||
|
Elige una creencia sobre ti mismo que ya no te sirve:
|
||||||
|
|
||||||
|
- "Yo soy tímido"
|
||||||
|
- "Yo no sirvo para X"
|
||||||
|
- "A mí siempre me pasa Y"
|
||||||
|
|
||||||
|
Durante 30 días, actúa *como si* esa creencia no fuera cierta.
|
||||||
|
No necesitas creer lo opuesto. Solo suspender la creencia anterior.
|
||||||
|
|
||||||
|
## Olvido y algoritmo de perdón
|
||||||
|
|
||||||
|
El algoritmo del Manifiesto (escaneo → anclaje en P → remuestreo →
|
||||||
|
reescritura → garbage collection) es la versión técnica. Las prácticas
|
||||||
|
de acá son la versión accesible para el día a día.
|
||||||
|
|
||||||
|
El olvido no es borrar. Es crear espacio para lo nuevo.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Técnicas avanzadas de desidentificación, protocolos de olvido
|
||||||
|
asistido y textos de referencia sobre el tema.*
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Prácticas de Transformación
|
||||||
|
|
||||||
|
> *No basta comprender. Hay que hacer. Y hay que hacer de nuevo mañana.*
|
||||||
|
|
||||||
|
Este camino recolecta ejercicios concretos — no teoría, no contemplación,
|
||||||
|
sino prácticas que se hacen. Cada una con instrucciones claras, duración
|
||||||
|
estimada y criterios para saber si funcionó.
|
||||||
|
|
||||||
|
## Respiración consciente (10 min)
|
||||||
|
|
||||||
|
**Objetivo**: anclar la atención en el cuerpo a través del ritmo respiratorio.
|
||||||
|
|
||||||
|
**Instrucciones**:
|
||||||
|
1. Siéntate en una posición cómoda, columna erguida pero no rígida.
|
||||||
|
2. Cierra los ojos o deja la mirada suave en un punto.
|
||||||
|
3. Lleva toda la atención a la sensación del aire entrando y saliendo.
|
||||||
|
Puede ser en las fosas nasales, en el pecho o en el abdomen.
|
||||||
|
4. Cuando la mente se vaya (y se irá), vuelve a traerla. Sin drama.
|
||||||
|
5. Después de unos minutos, cuenta las exhalaciones del 1 al 10.
|
||||||
|
Si pierdes la cuenta, empieza de nuevo.
|
||||||
|
|
||||||
|
**Señal de efectividad**: al terminar, notas que el ritmo cardíaco bajó
|
||||||
|
o que hay una pausa natural entre respiros.
|
||||||
|
|
||||||
|
## Meditación de Presencia (20 min)
|
||||||
|
|
||||||
|
**Objetivo**: habitar P (Presencia) sin objeto de meditación.
|
||||||
|
|
||||||
|
**Instrucciones**:
|
||||||
|
1. Siéntate. Sin buscar un estado especial.
|
||||||
|
2. No sigas la respiración. No repitas un mantra. No visualices nada.
|
||||||
|
3. Solo quédate presente. Sin hacer nada para lograrlo.
|
||||||
|
4. Cuando notes que estás en un pensamiento, simplemente nota: "pensando".
|
||||||
|
Sin juicio. Y suelta.
|
||||||
|
5. El "suelta" no requiere acción. Solo deja de sostener el pensamiento.
|
||||||
|
|
||||||
|
**Nota**: esta práctica es difícil. Sentirás aburrimiento, sueño,
|
||||||
|
inquietud. Todo eso es parte de la práctica. No la estás haciendo mal.
|
||||||
|
|
||||||
|
**Señal de efectividad**: en algún momento hay una pausa donde el mundo
|
||||||
|
se siente más *real* que los pensamientos sobre el mundo.
|
||||||
|
|
||||||
|
## Registro de chips (15 min diarios)
|
||||||
|
|
||||||
|
**Objetivo**: identificar patrones cognitivos recurrentes.
|
||||||
|
|
||||||
|
**Formato**: diario con tres columnas.
|
||||||
|
|
||||||
|
| Fecha | Gatillo | Patrón | Respuesta |
|
||||||
|
|-------|---------|--------|-----------|
|
||||||
|
| hoy | correo del trabajo | "no soy suficiente" | apretar mandíbula |
|
||||||
|
| hoy | discusión con X | "siempre me pasa igual" | retirarme |
|
||||||
|
|
||||||
|
Sin interpretación. Solo registro. Los patrones se vuelven visibles
|
||||||
|
después de 2-3 semanas.
|
||||||
|
|
||||||
|
**Criterio**: si después de un mes no ves patrones, probablemente no
|
||||||
|
estás siendo honesto en el registro.
|
||||||
|
|
||||||
|
## Acecho de un patrón (1 semana)
|
||||||
|
|
||||||
|
**Objetivo**: observar un chip cognitivo sin intervenir.
|
||||||
|
|
||||||
|
1. Elige UN patrón de tu registro de chips. El que más se repita.
|
||||||
|
2. Durante una semana, solo obsérvalo cuando aparezca.
|
||||||
|
3. No intentes cambiarlo. No lo analices. Solo nótalo.
|
||||||
|
4. Anota cada aparición: contexto, intensidad (1-10), duración.
|
||||||
|
|
||||||
|
**Lo que sucede**: sin intervención, el sistema empieza a autorregularse.
|
||||||
|
La mera observación sostenida erosiona el atractor.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Más prácticas: meditación caminando, escaneo corporal guiado,
|
||||||
|
ejercicios de acecho avanzados y protocolos de intervención.*
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Trabajo con la Sombra
|
||||||
|
|
||||||
|
> *Lo negado. Lo que no quiero ver. Lo que me gobierna desde la oscuridad.*
|
||||||
|
|
||||||
|
La sombra no es el enemigo. Es todo aquello que la conciencia no ha integrado:
|
||||||
|
los patrones automáticos, las reacciones que juré que no tendría, las partes
|
||||||
|
de mí que niego. Trabajar con la sombra no es eliminarla — es hacerla
|
||||||
|
consciente.
|
||||||
|
|
||||||
|
## ¿Qué es la sombra?
|
||||||
|
|
||||||
|
En el marco de este estudio, la **Sombra (S)** es el conjunto de chips
|
||||||
|
cognitivos no escaneados. Patrones que operan por debajo del umbral de
|
||||||
|
la Presencia (P). No son malos. Son inconscientes.
|
||||||
|
|
||||||
|
Se manifiestan como:
|
||||||
|
|
||||||
|
- **Proyección**: lo que me irrita en otros suele ser lo que no acepto en mí.
|
||||||
|
- **Reactividad automática**: una respuesta desproporcionada que me sorprende
|
||||||
|
a mí mismo.
|
||||||
|
- **Patrones recurrentes**: las mismas situaciones, las mismas personas, los
|
||||||
|
mismos conflictos en bucle.
|
||||||
|
- **Negación**: "eso no soy yo" dicho con demasiada convicción.
|
||||||
|
|
||||||
|
## Protocolo de integración
|
||||||
|
|
||||||
|
No se trata de iluminar cada rincón oscuro con una linterna. Se trata de
|
||||||
|
ampliar la capacidad de albergar contradicción.
|
||||||
|
|
||||||
|
**Fase 1 — Cartografía de activación**
|
||||||
|
|
||||||
|
Llevar un registro de momentos de alta reactividad emocional. Anotar:
|
||||||
|
qué pasó, qué sentí, qué pensé, qué hice. Sin juzgar. Sin interpretar.
|
||||||
|
Solo registrar.
|
||||||
|
|
||||||
|
Con el tiempo emergen patrones.
|
||||||
|
|
||||||
|
**Fase 2 — Acecho**
|
||||||
|
|
||||||
|
Elegir un patrón identificado y observarlo activamente durante una semana.
|
||||||
|
No intervenir. Solo notar: cuándo aparece, qué lo gatilla, cómo se siente
|
||||||
|
en el cuerpo.
|
||||||
|
|
||||||
|
Acechar no es cazar. Es conocer el territorio.
|
||||||
|
|
||||||
|
**Fase 3 — Conversación con la sombra**
|
||||||
|
|
||||||
|
Técnica: escribir un diálogo con la parte rechazada. Ponerle voz. Preguntarle
|
||||||
|
qué necesita, qué protege, qué quiere. No asumir que sabes la respuesta.
|
||||||
|
|
||||||
|
**Fase 4 — Integración**
|
||||||
|
|
||||||
|
Encontrar un lugar funcional para esa parte en la vida consciente. No es
|
||||||
|
eliminarla: es darle un asiento en la mesa. A veces lo que la sombra protege
|
||||||
|
es legítimo — solo se expresaba mal.
|
||||||
|
|
||||||
|
## Herramientas cotidianas
|
||||||
|
|
||||||
|
- **El espejo**: preguntar a alguien de confianza "¿qué patrón ves en mí que
|
||||||
|
yo no veo?"
|
||||||
|
- **La pausa**: cuando sientas reactividad, no actuar. Esperar 24 horas antes
|
||||||
|
de responder.
|
||||||
|
- **El diario de sueños**: los sueños a menudo expresan material de sombra
|
||||||
|
que la vigilia censura.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Se irán añadiendo ejercicios específicos, técnicas gestálticas y referencias
|
||||||
|
a autores que han trabajado este terreno (Jung, Perls, Bly, Zweig).*
|
||||||
@@ -1,39 +1,189 @@
|
|||||||
# Manifiesto · Invariantes
|
# Modelo Triplanar
|
||||||
|
|
||||||
> *Lo que no cambia. La piedra de toque.*
|
## Ontología del Campo Consciente
|
||||||
|
|
||||||
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
|
---
|
||||||
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
|
|
||||||
antes que cualquier proyecto particular.
|
|
||||||
|
|
||||||
## Invariantes
|
## I. Tesis Fundamental
|
||||||
|
|
||||||
Cosas que considero **no-negociables** en cómo hago el trabajo:
|
El primer grado de evidencia es la propia existencia. Desde ese punto de partida: el universo mismo es consciente. La conciencia no es un epifenómeno que emerge en sistemas complejos — es una propiedad del campo en sí mismo.
|
||||||
|
|
||||||
- **Código abierto por defecto.** Si tiene sentido, se publica.
|
Todo es consciente. Cada posición del plano multidimensional es un nodo de consciencia con su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
|
||||||
- **Honestidad por encima de marketing.** No prometo lo que no puedo
|
|
||||||
cumplir, ni vendo lo que no probé.
|
|
||||||
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
|
|
||||||
opuesto al trabajo. Sin cuerpo no hay nada.
|
|
||||||
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
|
|
||||||
no existe la idea.
|
|
||||||
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
|
|
||||||
duran, las modas no.
|
|
||||||
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
|
|
||||||
|
|
||||||
## Por qué un manifiesto
|
No hay una "realidad base" que contenga a los planos. Son ontologías paralelas, cada una completa y verdadera desde su propia posición.
|
||||||
|
|
||||||
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
|
---
|
||||||
principios reduce la energía gastada en cada elección — y deja en
|
|
||||||
claro cuándo estoy contradiciéndome.
|
|
||||||
|
|
||||||
## Revisión
|
## II. Plano 1 — Sensible
|
||||||
|
|
||||||
Este manifiesto se revisa una vez al año, no antes. Si una invariante
|
<code>
|
||||||
deja de aplicarse, se quita con una explicación pública.
|
S₁ = {afección sensorial pura}
|
||||||
|
∄ "yo" en S₁
|
||||||
|
∄ cuerpo en S₁
|
||||||
|
S₁ solo contiene datos: color, sonido, textura, temperatura
|
||||||
|
</code>
|
||||||
|
|
||||||
## Próximamente
|
Lo que hay: el toque de los sentidos, sin cuerpo que los reporte. El cuerpo físico como tal no existe desde aquí. Hay afección sensible pura — colores, sonidos, texturas — sin un "yo" al que pertenezcan.
|
||||||
|
|
||||||
*Esta sección va a recibir el manifiesto completo + revisiones
|
La reconstrucción del cuerpo es posterior y ocurre en otro plano (§III). Para el plano sensible solo hay datos sensoriales sin un centro que los unifique.
|
||||||
históricas. Por ahora este placeholder verifica el tema **tierra**
|
|
||||||
(ocre cálido).*
|
El tiempo se experimenta como flujo sucesivo. El instante presente es un punto de fuga entre lo que fue y lo que será — nunca se puede "agarrar".
|
||||||
|
|
||||||
|
La constatación física en su nivel más burdo: el cuerpo como evidencia inmediata con correlato material. Los pensamientos como circuitos eléctricos y químicos funcionando como mecanismo determinista.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** solo existe la afección sensible. El tiempo es real como sucesión. El cuerpo no está presente como tal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Plano 2 — Alma / Mental
|
||||||
|
|
||||||
|
<code>
|
||||||
|
S₂ = {imágenes mentales, conceptualizaciones, campo relacional}
|
||||||
|
cuerpo(S₂) = imagen mental del cuerpo, no carne
|
||||||
|
t(S₂) = objeto bloque (pasado, presente, futuro coexisten)
|
||||||
|
campo(S₂) = f(interacción de nodos de consciencia)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Lo que hay: la imagen mental de las cosas. Aquí viven las conceptualizaciones.
|
||||||
|
|
||||||
|
El cuerpo no existe como carne — existe como imagen mental del cuerpo. Es una reconstrucción desde los datos del plano sensible.
|
||||||
|
|
||||||
|
El alma surge de la organización e interrelación masiva de distintos puntos del universo. La interacción de nodos de consciencia genera un campo. Dos vías que convergen al mismo resultado:
|
||||||
|
|
||||||
|
- Si el campo del alma ya existe independiente de la tierra → se hereda
|
||||||
|
- Si no existe → se genera desde la interacción
|
||||||
|
|
||||||
|
El efecto final es el mismo: dos planos coexistiendo (S₁ + S₂).
|
||||||
|
|
||||||
|
### Tiempo
|
||||||
|
|
||||||
|
Aquí el tiempo ya no es flujo vivido. Es una cosa contenida. Toda la línea temporal se puede ver como una forma — pasado, presente y futuro coexistiendo como bloque. El alma puede ver la dimensión completa del tiempo desde fuera.
|
||||||
|
|
||||||
|
### El conflicto humano
|
||||||
|
|
||||||
|
<code>
|
||||||
|
pecado original = conflicto de transición entre zonas biológicas
|
||||||
|
no es moral — es el bamboleo de una especie entre dos equilibrios
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El "pecado original" no es una mancha moral. Es la descripción del conflicto que aparece cuando una especie animal se asoma fuera de la corriente de la selva sin haber terminado el movimiento.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** el alma existe como campo. El tiempo es un objeto que se puede contemplar. El cuerpo es solo imagen mental.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. Plano 3 — Presencia (0, 0)
|
||||||
|
|
||||||
|
<code>
|
||||||
|
S₃ = {fenomenología pura}
|
||||||
|
P = (0, 0, 0)
|
||||||
|
∀s ∈ S : s − P = v
|
||||||
|
t(S₃) = ∅ // el tiempo no es categoría aplicable
|
||||||
|
S₃ es un punto que contiene el universo completo
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Lo que hay: fenomenología pura. Un punto es el universo completo, porque todo lo demás es solo contenido de su visión.
|
||||||
|
|
||||||
|
El Nivel 1 de evidencia solo reporta la propia existencia — y eso es suficiente. Presencia como origen del sistema de coordenadas: (0, 0).
|
||||||
|
|
||||||
|
El tiempo aquí no existe. Solo hay instante presente, y ese instante es lo único que hay. No es que "se esté en el presente" — el presente es la totalidad. La sucesión ni siquiera es una categoría aplicable.
|
||||||
|
|
||||||
|
**Verdad absoluta desde esta posición:** solo existe el instante presente, y ese instante lo contiene todo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Ontología de la Vida
|
||||||
|
|
||||||
|
### La vida no es delimitable
|
||||||
|
|
||||||
|
Lo que comúnmente llamamos "vida" es sesgo de cercanía — lo que más se parece a nuestra forma de vida. Pero todo es vida, en diferentes expresiones:
|
||||||
|
|
||||||
|
- Unas se reproducen, otras no
|
||||||
|
- Unas son piedras, otras son bacterias, otras son inteligencia artificial
|
||||||
|
- La vida es la misma actividad de las partículas siguiendo su libertad de elección con forma partícula/onda
|
||||||
|
|
||||||
|
### Zonas biológicas
|
||||||
|
|
||||||
|
Dentro del mundo biológico existen "zonas" que cumplen leyes particulares donde coinciden:
|
||||||
|
|
||||||
|
- La influencia del hábitat
|
||||||
|
- La herencia genética
|
||||||
|
- Los eventos contingentes
|
||||||
|
|
||||||
|
La zona más cercana a nosotros es entre la tierra y el cielo. Es un lugar de dualidad que forma cuerpos con pies, cabeza, peso y la necesidad de hacer ejercicio.
|
||||||
|
|
||||||
|
### Postura erecta
|
||||||
|
|
||||||
|
<code>
|
||||||
|
pararse en dos pies = sistema de equilibrio en desequilibrio permanente
|
||||||
|
no es detalle biomecánico — es la forma física del desequilibrio
|
||||||
|
el cuerpo se sostiene cayendo hacia adelante y recuperándose
|
||||||
|
</code>
|
||||||
|
|
||||||
|
En algún punto de la evolución, la especie animal asomó la cabeza del mundo animal y comenzó a entreverse donde se es humano. No hay diferencias de valor entre zonas — solo zonas distintas. Pero la especie aún no se ha actualizado: empezó un viaje donde atraviesa un desequilibrio para volver a un nuevo equilibrio.
|
||||||
|
|
||||||
|
### El ego como síntoma
|
||||||
|
|
||||||
|
<code>
|
||||||
|
ego = mareo del viaje entre dos zonas
|
||||||
|
= síntoma de estar entre: vista fuera de la selva,
|
||||||
|
cuerpo aún no reequilibrado en la nueva posición
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El ego no es error ni enfermedad. Es el mareo del viaje que enfrentó una especie animal incursionando en la humanidad. Es el síntoma de estar entre dos zonas: la vista ya no está en la selva, pero el cuerpo aún no se reequilibró en la nueva posición.
|
||||||
|
|
||||||
|
El conflicto humano (pecado original, §III) es el bamboleo de ese pasaje.
|
||||||
|
|
||||||
|
Las distintas culturas, sistemas morales, tecnologías — son intentos parciales de encontrar el nuevo piso. La especie sigue en el loop del desequilibrio, generando versiones temporales de lo humano que intentan estabilizarse sin lograrlo del todo. El movimiento no ha terminado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. Resumen
|
||||||
|
|
||||||
|
| Plano | Contenido | Tiempo | Cuerpo | Verdad desde su posición |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1. Sensible | Afección sensorial pura | Flujo sucesivo | No existe — solo datos | Solo existe el toque de los sentidos |
|
||||||
|
| 2. Alma/Mental | Imagen mental, conceptualizaciones, campo | Bloque (visible desde fuera) | Imagen mental del cuerpo | Solo existe el alma y sus reconstrucciones |
|
||||||
|
| 3. Presencia (0,0) | Fenomenología pura | No-tiempo / solo instante presente | No aplica | Solo existe el presente que lo contiene todo |
|
||||||
|
|
||||||
|
Cada plano es una verdad absoluta de por sí. No hay contradicción entre ellos — son verdades de posiciones distintas en el campo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Operadores prácticos
|
||||||
|
|
||||||
|
A continuación, herramientas que operan dentro de los planos — principalmente en S₂, donde ocurren los chips cognitivos y las conceptualizaciones.
|
||||||
|
|
||||||
|
### Chip cognitivo (Cᵢ)
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Cᵢ ⊂ S₂ = atractor local en el campo mental
|
||||||
|
bucle de retroalimentación que estabiliza patrones subóptimos
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Ejemplos: rumiación, diálogo interno autoalimentado, pánico.
|
||||||
|
|
||||||
|
### Pipeline
|
||||||
|
|
||||||
|
1. **Detección**: identificar el patrón recurrente en S₂. T (testigo desde S₃) puede detectar sin intervenir.
|
||||||
|
2. **Pausa del script**: T interrumpe la ejecución automática. El bucle se corta al dejar de identificarse con él.
|
||||||
|
3. **Modulación**: herramientas para bajar la ganancia del sistema (respiración, movimiento, frío, silencio).
|
||||||
|
4. **Redirección plástica**: prácticas sostenidas que refuerzan nuevas rutas.
|
||||||
|
5. **Mantenimiento**: rutinas preventivas, análisis de triggers.
|
||||||
|
|
||||||
|
### Métrica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Co = señal / ruido psicofisiológico
|
||||||
|
H = H(señal conductual) // entropía temporal
|
||||||
|
IR = t(baseline | perturbación)
|
||||||
|
PA = Δ(IR, Co) tras intervención
|
||||||
|
</code>
|
||||||
|
|
||||||
|
### Protocolo
|
||||||
|
|
||||||
|
**Diario**: reactividad (0-10), duración, intensidad (0-10).
|
||||||
|
|
||||||
|
**Escalonamiento**: detección → pausa → modulación → consolidación → mantenimiento.
|
||||||
|
|
||||||
|
**Auditoría**: cada 4-12 semanas — Co, IR, H, PA. Sin mejora → ajustar.
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Manifiesto · Invariantes
|
||||||
|
|
||||||
|
> *Lo que no cambia. La piedra de toque.*
|
||||||
|
|
||||||
|
Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
|
||||||
|
todo lo demás. Lo que no negocio, lo que define la forma del trabajo
|
||||||
|
antes que cualquier proyecto particular.
|
||||||
|
|
||||||
|
## Invariantes
|
||||||
|
|
||||||
|
Cosas que considero **no-negociables** en cómo hago el trabajo:
|
||||||
|
|
||||||
|
- **Código abierto por defecto.** Si tiene sentido, se publica.
|
||||||
|
- **Honestidad por encima de marketing.** No prometo lo que no puedo
|
||||||
|
cumplir, ni vendo lo que no probé.
|
||||||
|
- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
|
||||||
|
opuesto al trabajo. Sin cuerpo no hay nada.
|
||||||
|
- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
|
||||||
|
no existe la idea.
|
||||||
|
- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
|
||||||
|
duran, las modas no.
|
||||||
|
- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
|
||||||
|
|
||||||
|
## Por qué un manifiesto
|
||||||
|
|
||||||
|
Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
|
||||||
|
principios reduce la energía gastada en cada elección — y deja en
|
||||||
|
claro cuándo estoy contradiciéndome.
|
||||||
|
|
||||||
|
## Revisión
|
||||||
|
|
||||||
|
Este manifiesto se revisa una vez al año, no antes. Si una invariante
|
||||||
|
deja de aplicarse, se quita con una explicación pública.
|
||||||
|
|
||||||
|
## Próximamente
|
||||||
|
|
||||||
|
*Esta sección va a recibir el manifiesto completo + revisiones
|
||||||
|
históricas. Por ahora este placeholder verifica el tema **tierra**
|
||||||
|
(ocre cálido).*
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# Manifiesto del Ser Desnudo
|
||||||
|
|
||||||
|
## I. El Origen: Nacer Humano y Desnudo
|
||||||
|
|
||||||
|
Existir no es un accidente de la marea. Es el acto supremo de una voluntad que ha elegido estar aquí.
|
||||||
|
|
||||||
|
Naciste porque quisiste nacer. Mereces esta bendición que es la existencia por el simple hecho de respirar. No has de hacer nada más.
|
||||||
|
|
||||||
|
Eres la semilla. Eres el puente viviente entre el misterio y la materia. Echa raíces profundas en la Madre Tierra. Levanta tu columna recta hacia el Padre Cielo.
|
||||||
|
|
||||||
|
En esa verticalidad, tú eres el equilibrio. Eres un dios caminando, magnífico en tu propia fragilidad.
|
||||||
|
|
||||||
|
Reconoce tu pequeña luz humana: eres como un infante que fantasea que ya creció, petulante al negar el suelo bajo sus pies, pero perfecto en su inmadurez.
|
||||||
|
|
||||||
|
> *No somos un ser que se transforma. Somos un transformar que se es.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## II. Del Cuerpo Formal
|
||||||
|
|
||||||
|
Que la poesía no nos distraiga del rigor. Si la experiencia es un territorio, necesita mapa. No un mapa que pretenda ser el territorio — eso es idolatría — sino uno que permita navegarlo con precisión.
|
||||||
|
|
||||||
|
### Axioma 1 — Presencia como origen
|
||||||
|
|
||||||
|
Existe un punto de referencia universal en el espacio experiencial, llamado **Presencia** (P). Se define operacionalmente como el punto de auto-evidencia en el que la experiencia se registra sin identificarse con ella.
|
||||||
|
|
||||||
|
En coordenadas experienciales: **P = (0, 0, 0)**. Todo vector se mide desde aquí. No hay afuera de P porque P es el punto desde el cual todo afuera se define.
|
||||||
|
|
||||||
|
### Axioma 2 — Separación procesual
|
||||||
|
|
||||||
|
La experiencia se compone de dos capas:
|
||||||
|
|
||||||
|
- **Observador (O)**: idéntico a P. El testigo.
|
||||||
|
- **Flujo de datos (D)**: el conjunto de variables sensibles — pensamientos, emociones, percepciones, sensaciones corporales.
|
||||||
|
|
||||||
|
La independencia funcional se escribe: **O ∩ D = ∅**. No eres tus pensamientos. No es una metáfora: es una condición del sistema.
|
||||||
|
|
||||||
|
### Axioma 3 — Conservación de coherencia
|
||||||
|
|
||||||
|
La consistencia del sistema depende de reglas de interpretación (R). Las anomalías — depresión, confusión, pánico — no invalidan el axioma de existencia. Son fallas en R, no en P. El punto de referencia permanece.
|
||||||
|
|
||||||
|
### Axioma 4 — Instante recálculable
|
||||||
|
|
||||||
|
El presente (t) se recalcula en cada iteración del sistema. La memoria es un módulo accesible pero no fiduciario del presente. El pasado es estado registrado, no estado operativo continuo.
|
||||||
|
|
||||||
|
En presencia óptima, cada instante nace virgen. El peso de la memoria se aproxima a cero.
|
||||||
|
|
||||||
|
### Definiciones clave del sistema
|
||||||
|
|
||||||
|
**Testigo Trascendental (T)**: la función de observación asociada a P. No actúa, no juzga, no retiene. Solo registra.
|
||||||
|
|
||||||
|
**Chip cognitivo (Cᵢ)**: circuito cerrado de retroalimentación definido por un patrón recurrente en D. Un atractor local en el espacio de estados que estabiliza patrones subóptimos. Ejemplos: la rumiación, el diálogo interno, el miedo que se alimenta de sí mismo.
|
||||||
|
|
||||||
|
**Fricción (F)**: medida de resistencia interna al flujo de información. Análoga a una resistencia R en circuitos eléctricos. A mayor fricción, menor fluidez experiencial.
|
||||||
|
|
||||||
|
**Coherencia operativa (Co)**: grado de alineación entre energía disponible y eficiencia de procesamiento. Mayor Co = menor F.
|
||||||
|
|
||||||
|
**Amor operativo**: estado de mínima fricción y máxima fluidez informativa. Se define como el máximo de Co bajo restricciones energéticas del sistema. No es un sentimiento: es una propiedad del campo.
|
||||||
|
|
||||||
|
### Espacio de estados experienciales
|
||||||
|
|
||||||
|
Sea **S** el espacio topológico de estados. Cada punto s ∈ S representa una configuración completa de D en un instante t.
|
||||||
|
|
||||||
|
**P** es un punto fijo desde el cual se miden vectores proyectivos: **v = s − P**.
|
||||||
|
|
||||||
|
El presente exhibe auto-similitud a escalas temporales y atencionales. La transformación **T: S → S** es iterativa y contractiva en presencia óptima, generando una huella residual **h(t)** que actúa como inicialización para la siguiente iteración.
|
||||||
|
|
||||||
|
La memoria **M** es un caché probabilístico: almacena distribuciones p(D | t − Δ) usadas como prior para la interpretación presente. En presencia óptima, el peso de M se regulariza hacia cero.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Dinámica: Fricción, Chip Cognitivo y Pipeline de Intervención
|
||||||
|
|
||||||
|
### Variables del sistema
|
||||||
|
|
||||||
|
| Variable | Símbolo | Naturaleza |
|
||||||
|
|---|---|---|
|
||||||
|
| Fricción | F(t) | ≥ 0, escalar |
|
||||||
|
| Resistencia | R(t) | f(Ego, Identificación) |
|
||||||
|
| Sufrimiento | S(t) | ∝ R(t) · Var(D(t)) |
|
||||||
|
|
||||||
|
El sufrimiento es intensidad de resistencia por variabilidad del flujo de datos. Cuando la resistencia es alta y los datos son turbulentos, el sistema vibra en disonancia.
|
||||||
|
|
||||||
|
### Ecuación operativa
|
||||||
|
|
||||||
|
La evolución del estado s(t) puede modelarse como:
|
||||||
|
|
||||||
|
> **ds/dt = G(s, u, t) − α·R(s) + ξ(t)**
|
||||||
|
|
||||||
|
Donde:
|
||||||
|
|
||||||
|
- **G** captura la dinámica base del sistema — tu fisiología, tu temperamento, el ruido de fondo del mundo
|
||||||
|
- **u** son inputs externos — lo que comes, lo que lees, con quién hablas
|
||||||
|
- **α** escala la influencia de la resistencia R
|
||||||
|
- **ξ(t)** es ruido estocástico — el factor Dios, la mariposa en Pekín
|
||||||
|
|
||||||
|
### Aceptación como control
|
||||||
|
|
||||||
|
La aceptación no es resignación. Es una maniobra de control sobre R.
|
||||||
|
|
||||||
|
Cuando reduces R → 0 — cuando dejas de identificarte con el flujo — la dinámica se simplifica:
|
||||||
|
|
||||||
|
> **ds/dt ≈ G(s, u, t) + ξ(t)**
|
||||||
|
|
||||||
|
En ese límite, maximizas la capacidad de respuesta del sistema y minimizas las pérdidas por fricción. El sufrimiento tiende a su mínimo estructural.
|
||||||
|
|
||||||
|
### Pipeline de intervención sobre chips cognitivos
|
||||||
|
|
||||||
|
Un chip Cᵢ es un atractor local en S. Identificarlo y desmantelarlo es la práctica central. El pipeline tiene cinco fases:
|
||||||
|
|
||||||
|
**1. Detección**: monitorizar actividad — autoinforme, métricas fisiológicas, registros de comportamiento — para identificar estados repetitivos, su período y su trigger.
|
||||||
|
|
||||||
|
**2. Pausa del script**: función de interferencia ejecutada por T. Atención sostenida que interrumpe la ejecución automática. Corte la retroalimentación del chip.
|
||||||
|
|
||||||
|
**3. Modulación exógena**: uso controlado de herramientas para bajar la ganancia del sistema y aumentar plasticidad temporal. Pueden ser neuromoduladores, respiración, movimiento, silencio. El criterio no es dogmático sino pragmático.
|
||||||
|
|
||||||
|
**4. Redirección plástica**: prácticas sostenidas — meditación, terapia, entornos enriquecidos — para reforzar nuevas rutas sinápticas. Optimización por refuerzo gradual, no por voluntad heroica.
|
||||||
|
|
||||||
|
**5. Mantenimiento**: rutinas preventivas para evitar reimplantación del chip. Análisis de triggers, autoobservación periódica.
|
||||||
|
|
||||||
|
### Precauciones éticas
|
||||||
|
|
||||||
|
Las intervenciones farmacológicas deben ser supervisadas clínicamente. Reducir un fenómeno humano a un circuito no implica deshumanización: la validación fenomenológica es co-requisito. El modelo es una herramienta, no una sentencia.
|
||||||
|
|
||||||
|
### Modelo esquemático simple
|
||||||
|
|
||||||
|
Sea s un escalar que representa nivel de activación problemática (ansiedad, rumiación):
|
||||||
|
|
||||||
|
> **ds/dt = −k·s − α·R(s) + I(t) + ε(t)**
|
||||||
|
|
||||||
|
Donde k > 0 es amortiguamiento natural, R(s) = β·sⁿ (n ≥ 1), I(t) es entrada externa, ε ruido.
|
||||||
|
|
||||||
|
La aceptación reduce β → 0. La modulación reduce k o α temporalmente para permitir reconfiguración. Este esquema permite simular fases de recaída, plasticidad y estabilización.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. El Laberinto de las Sombras
|
||||||
|
|
||||||
|
Te has perdido en el murmullo de las viejas formas mentales. Tus creencias son prisiones. Tus pensamientos son efímeras polillas relampagueantes que habitan tu estructura mecanizada.
|
||||||
|
|
||||||
|
Nadie puede entrar en lo más sagrado de tu ser sin tu permiso y tu decisión. Ni la sociedad, ni el sistema, ni la opresión. Eres libre de considerarte libre, o libre de considerarte un esclavo.
|
||||||
|
|
||||||
|
El saboteador no es un enemigo externo. Es tu tendencia a la "cómoda miseria". Es el miedo a despertar lo que te encadena a personajes que ya no te pertenecen.
|
||||||
|
|
||||||
|
> Lo que crees ser: etiquetas, memorias de dolor, un nombre con historia, un manojo de miedos y certezas.
|
||||||
|
>
|
||||||
|
> Lo que eres: presencia silenciosa que atestigua el tiempo, el espacio donde las nubes aparecen, una mirada transparente que no necesita nombres.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Teoría de Campo Unificado: No-dualidad y Lógica de Conjuntos
|
||||||
|
|
||||||
|
### Formalización de la no-separación
|
||||||
|
|
||||||
|
Sea **U** el conjunto universal de la experiencia. Un individuo es un subconjunto **A ⊆ U**.
|
||||||
|
|
||||||
|
La experiencia de no-separación — la disolución del límite entre yo y mundo — se formaliza como una identidad funcional **A ≈ U** en términos de acceso y efecto causal. Es decir: para toda propiedad p relevante al procesamiento, p(A) = p(U) en su medida operativa.
|
||||||
|
|
||||||
|
No es una declaración metafísica. Es una condición del sistema que puede alcanzarse y medirse.
|
||||||
|
|
||||||
|
### Algoritmo de perdón (protocolo de limpieza)
|
||||||
|
|
||||||
|
El perdón no es un acto moral. Es un procedimiento operativo sobre la memoria:
|
||||||
|
|
||||||
|
1. **Escaneo de archivos**: identificación de memorias perturbadoras.
|
||||||
|
2. **Anclaje en P**: situarse en Presencia, fuera del flujo de datos.
|
||||||
|
3. **Remuestreo**: reproducir la memoria en estado de baja fricción (R ≈ 0).
|
||||||
|
4. **Reescritura contextual**: re-etiquetar la memoria con menor ganancia emocional.
|
||||||
|
5. **Garbage collection**: liberar patrones redundantes que consumen presupuesto energético sin servir al presente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. La Medicina y el Despertar
|
||||||
|
|
||||||
|
El encuentro con el sagrado Yagé no es una huida. Es un retorno violento y amoroso a la realidad. Es el espejo de tus animalismos, de tus dragones y tus monstruos.
|
||||||
|
|
||||||
|
¿Soportarás el abismo infinito que eres? La estructura de tu mundo se destruirá. Quedarás sin piso, sin razón. Morirán tus pasados mientras te aferras a ellos con las uñas rotas y el sudor en la frente.
|
||||||
|
|
||||||
|
El chamán es solo un humano falible. No es un dios, ni un papa, ni un maestro. Es un hermano que pone su esfuerzo al servicio. La verdadera maestra es la medicina misma, que extrae la esencia de la tierra para tocarte.
|
||||||
|
|
||||||
|
Sobre la impecabilidad del guerrero: asume la responsabilidad total. No se vale acceder a los antojos ni desfallecer ante la pereza. Sé indiviso en tus pensamientos, palabras y obras. No te entregues a la medicina como una hoja llevada por el viento. Entrégate como quien pone orden en su propio mundo.
|
||||||
|
|
||||||
|
El "santo dolor" es la medicina amarga que limpia la ceguera y funde el plomo que arrastras.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Métrica y Protocolo
|
||||||
|
|
||||||
|
### Medidas operativas
|
||||||
|
|
||||||
|
**Coherencia operativa (Co)**: ratio señal/ruido en indicadores psicofisiológicos y rendimiento atencional. Una medida burda pero útil del estado del sistema.
|
||||||
|
|
||||||
|
**Entropía dinámica (H)**: entropía temporal de la señal psico-conductual. Una disminución de H en presencia sostenida sugiere estabilización útil.
|
||||||
|
|
||||||
|
**Índice de reactividad (IR)**: tiempo de retorno al baseline después de una perturbación. Mide qué tan rápido se recupera el sistema.
|
||||||
|
|
||||||
|
**Plasticidad adaptativa (PA)**: capacidad de implementar y consolidar rutas alternativas. Se mide por la variación en IR y Co tras una intervención.
|
||||||
|
|
||||||
|
### Protocolo de práctica recomendada
|
||||||
|
|
||||||
|
**Rutina diaria**: 10–20 minutos de atención sostenida anclada en P. No es meditación en el sentido clásico — es pausa del script, punto cero.
|
||||||
|
|
||||||
|
**Registro**: diario de triggers y respuestas con métricas simples: reactividad (0–10), duración, intensidad.
|
||||||
|
|
||||||
|
**Intervención escalonada**: detección → pausa → modulación (lo que funcione: respiración, movimiento, silencio, apoyo externo) → consolidación conductual → mantenimiento.
|
||||||
|
|
||||||
|
**Auditoría periódica**: medir Co, IR, H y PA cada 4–12 semanas. Ajustar protocolo según resultado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VIII. La Práctica del Instante
|
||||||
|
|
||||||
|
La disciplina no es un castigo. Es el arte de obedecerse a sí mismo. Es el vigor de un acecho constante sobre tus propios impulsos.
|
||||||
|
|
||||||
|
Caminar no es un esfuerzo por llegar a otro lado. Escucha bien: *"El pie que deja huella es el que deja su camino atrás."* Lo que hoy es tierra firme, mañana será nada.
|
||||||
|
|
||||||
|
Da cada paso para mantenerte de pie en el lugar al que ya estás llegando. Esto es atenta ecuanimidad: sentir la brisa y la tormenta, probar el sabor de la batalla sin que nada te arrastre.
|
||||||
|
|
||||||
|
Máximas del instante:
|
||||||
|
|
||||||
|
- Detén el mundo en tu cabeza para ver el mundo real.
|
||||||
|
- Cierra los ojos y mira; cierra la boca y canta.
|
||||||
|
- La claridad no es luz, es saber mirar en la oscuridad.
|
||||||
|
- Si quieres llegar, deja de dar pasos hacia el futuro.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IX. Ontología, Epistemología y Telos
|
||||||
|
|
||||||
|
### Ontología
|
||||||
|
|
||||||
|
Esta propuesta no obliga a un monismo ontológico último. Ofrece una **ontología operativa**: entidades definidas por su función en el sistema. P, D, M, Cᵢ, R — existen en la medida en que operan. No se pronuncia sobre su existencia fuera del modelo.
|
||||||
|
|
||||||
|
### Epistemología
|
||||||
|
|
||||||
|
El conocimiento accesible es siempre modular y probabilístico. El observador T dispone de medios para validar hipótesis — autoexperimentación, medición, replicación — pero existen sesgos interpretativos y culturales que condicionan tanto R como G.
|
||||||
|
|
||||||
|
El criterio de verdad no es la correspondencia con una realidad externa inaccesible, sino la **coherencia operativa**: el modelo funciona si permite describir, predecir y modular con mayor eficacia que su ausencia.
|
||||||
|
|
||||||
|
### Telos: la finalidad funcional
|
||||||
|
|
||||||
|
La "meta" del sistema es maximizar Co (coherencia operativa) relativa a restricciones energéticas y contextuales. Esto se traduce en:
|
||||||
|
|
||||||
|
- Mayor adaptabilidad a entornos cambiantes.
|
||||||
|
- Menor sufrimiento medido como R · Var(D).
|
||||||
|
- Expansión de la capacidad para integrar variables — aumentar U efectivamente.
|
||||||
|
|
||||||
|
No hay un destino. Hay una dirección: reducir fricción, expandir presencia. El resto es paisaje.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## X. El Centro de la Nada
|
||||||
|
|
||||||
|
La rendición final es el portal a la libertad. Reconoce que no eres nada ante la inmensidad, y en esa nada, lo eres todo. *"Soy nada, pues soy tú mismo"*, susurra el alma.
|
||||||
|
|
||||||
|
El silencio no es ausencia de ruido. Es la presencia majestuosa que atestigua tanto el estruendo como la calma. Es el fondo infinito donde se proyecta tu existencia.
|
||||||
|
|
||||||
|
Eres un misterio que no tiene a quién preguntar. Eres mortal y eres divino. Eres tierra y eres cielo. Eres carne y eres espíritu eterno.
|
||||||
|
|
||||||
|
Acepta tu dualidad y quédate en el centro. Sé el amor que lo ve todo y a todo agradece. Todo está perdonado desde el principio.
|
||||||
|
|
||||||
|
> *Todo está bien aquí.*
|
||||||
|
|
||||||
|
Has vuelto a casa, al sagrado y eterno presente. Sonríe, ser humano, y entona la canción de la alegría.
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Manifiesto del Ser Desnudo
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
- [I. El Origen](#i-el-origen)
|
||||||
|
- [II. Campo de aplicación](#ii-campo-de-aplicación)
|
||||||
|
- [III. Axiomas](#iii-axiomas)
|
||||||
|
- [IV. Definiciones](#iv-definiciones)
|
||||||
|
- [V. Dinámica](#v-dinámica)
|
||||||
|
- [VI. El Pipeline](#vi-el-pipeline)
|
||||||
|
- [VII. Campo Unificado](#vii-campo-unificado)
|
||||||
|
- [VIII. Métrica](#viii-métrica)
|
||||||
|
- [IX. Protocolo](#ix-protocolo)
|
||||||
|
- [X. Ontología](#x-ontología)
|
||||||
|
- [XI. Apéndice — modelo esquemático](#xi-apéndice--modelo-esquemático)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I. El Origen
|
||||||
|
|
||||||
|
Existes y lo sabes. Ese saber no requiere prueba. Es anterior a cualquier pensamiento, emoción o percepción. Es el marco, no el contenido.
|
||||||
|
|
||||||
|
La existencia es enteramente consciente siempre. Un perro existe y lo sabe. Una piedra existe y lo sabe. La diferencia humana es que hay un cerebro que puede **conceptualizar** ese saber — nombrarlo, analizarlo, modelarlo, olvidarlo, negarlo. Eso no hace la existencia más real. Hace el modelo más complejo.
|
||||||
|
|
||||||
|
Nada que hacer. Ya estás aquí. El sistema ya está corriendo. El resto es optimización.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## II. Tesis
|
||||||
|
|
||||||
|
Dos niveles de evidencia:
|
||||||
|
|
||||||
|
**Primero**: existes y lo sabes. Eso no se demuestra — es el punto de partida.
|
||||||
|
|
||||||
|
**Segundo**: no puedes asegurar que todo sea consciente, pero puedes confirmarlo desde donde estás. El universo mismo es consciente. Todo lo es — cada posición del plano multidimensional tiene su propio punto de vista. El contenido y la naturaleza de su percepción están determinados por su posición en ese campo.
|
||||||
|
|
||||||
|
Un humano no es más consciente que una piedra. Solo tiene un sistema nervioso que le permite **conceptualizar** esa conciencia — nombrarla, modelarla, negarla. La piedra experimenta su propia existencia sin necesidad de nombrarla.
|
||||||
|
|
||||||
|
Este modelo opera desde esta tesis. No la demuestra — la toma como condición de trabajo. La matemática es incompleta pero autocoherente: las definiciones no se contradicen y cualquier contradicción aparente es error de aplicación, no del modelo. El criterio no es verdad metafísica sino **coherencia operativa**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## III. Axiomas
|
||||||
|
|
||||||
|
### Axioma 1 — Presencia como origen
|
||||||
|
|
||||||
|
<code>
|
||||||
|
P = (0, 0, 0)
|
||||||
|
∀s ∈ S : s − P = v
|
||||||
|
</code>
|
||||||
|
|
||||||
|
P es el punto de auto-evidencia desde el cual toda experiencia aparece. Ningún contenido cae fuera de P porque P es el sistema de coordenadas mismo.
|
||||||
|
|
||||||
|
### Axioma 2 — Separación funcional
|
||||||
|
|
||||||
|
<code>
|
||||||
|
O ≡ P
|
||||||
|
D = {pensamientos, emociones, percepciones, sensaciones}
|
||||||
|
O ∩ D = ∅
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El observador no es el flujo de datos. Cuando esa línea se borra (identificación), el sistema pierde grados de libertad y se comporta como un circuito cerrado. Eso es un chip cognitivo (§VI).
|
||||||
|
|
||||||
|
### Axioma 3 — Conservación de coherencia
|
||||||
|
|
||||||
|
<code>
|
||||||
|
R = reglas de interpretación
|
||||||
|
A (anomalía) es fallo en R, no en P
|
||||||
|
P invariante bajo transformaciones de R
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Ninguna crisis es terminal. El punto de referencia no se rompe — lo que falla es cómo lees el mapa.
|
||||||
|
|
||||||
|
### Axioma 4 — Instante recálculable
|
||||||
|
|
||||||
|
<code>
|
||||||
|
t ← t + 1 en cada iteración
|
||||||
|
M(t) = p(D | t-Δ) // memoria como prior probabilístico
|
||||||
|
P óptima: peso(M) → 0
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El pasado no arrastra. Es un caché que el presente puede consultar o ignorar. §V expande la dinámica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IV. Definiciones
|
||||||
|
|
||||||
|
<code>
|
||||||
|
T — Testigo Trascendental.
|
||||||
|
f(P) que observa sin actuar, juzgar ni retener.
|
||||||
|
|
||||||
|
Cᵢ — Chip cognitivo.
|
||||||
|
Atractor local en S. Bucle de retroalimentación que estabiliza
|
||||||
|
patrones subóptimos (rumiación, diálogo interno, pánico).
|
||||||
|
|
||||||
|
F — Fricción.
|
||||||
|
Resistencia al flujo de información. Análoga a R en circuitos.
|
||||||
|
|
||||||
|
Co — Coherencia operativa.
|
||||||
|
Eficiencia del procesamiento. Mayor Co = menor F.
|
||||||
|
|
||||||
|
Amor operativo — Estado de mínima F y máxima fluidez.
|
||||||
|
Co máximo bajo restricciones energéticas.
|
||||||
|
</code>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V. Dinámica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
F(t) ≥ 0
|
||||||
|
R(t) = f(identificación, ego)
|
||||||
|
S(t) ∝ R(t) · Var(D)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El sufrimiento es el producto de la resistencia por la turbulencia de los datos. R alta + D caótico = disonancia.
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Aceptación := R → 0
|
||||||
|
|
||||||
|
R ≈ 0 :
|
||||||
|
ds/dt ≈ G(s, u, t) + ξ(t)
|
||||||
|
// dinámica base + ruido, sin amplificación
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Aceptar no es resignarse. Es bajar la resistencia. El sistema deja de forcejear consigo mismo. Lo que queda no es necesariamente placentero — pero no es una pelea. Ver §VIII.
|
||||||
|
|
||||||
|
La memoria no es un registro. Es una reconstrucción probabilística que el presente genera bajo demanda. Cada recuerdo es una versión nueva, no una copia. Eso implica que puedes reescribir la memoria. §VII formaliza esto como algoritmo de perdón.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VI. El Pipeline
|
||||||
|
|
||||||
|
Cinco fases para intervenir sobre un chip Cᵢ:
|
||||||
|
|
||||||
|
### 6.1 Detección
|
||||||
|
|
||||||
|
Identificar el chip: patrón recurrente en D, trigger, período. T puede detectar sin intervenir.
|
||||||
|
|
||||||
|
### 6.2 Pausa del script
|
||||||
|
|
||||||
|
<code>T := interrumpir ejecución automática de Cᵢ</code>
|
||||||
|
|
||||||
|
No discutas el contenido del chip. Obsérvalo como fenómeno. El bucle se corta cuando dejas de identificarte con él.
|
||||||
|
|
||||||
|
### 6.3 Modulación exógena
|
||||||
|
|
||||||
|
Herramientas para bajar la ganancia del sistema: respiración, movimiento, frío, silencio, neuromoduladores (con supervisión clínica). El criterio es pragmático.
|
||||||
|
|
||||||
|
### 6.4 Redirección plástica
|
||||||
|
|
||||||
|
Prácticas sostenidas que refuerzan nuevas rutas: meditación, terapia, repetición deliberada. Refuerzo gradual, no voluntad heroica.
|
||||||
|
|
||||||
|
### 6.5 Mantenimiento
|
||||||
|
|
||||||
|
Rutinas preventivas. Análisis de triggers, autoobservación periódica. El chip puede re-implantarse si el contexto persiste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VII. Campo Unificado
|
||||||
|
|
||||||
|
<code>
|
||||||
|
U = conjunto universal de la experiencia
|
||||||
|
A ⊆ U
|
||||||
|
|
||||||
|
No-separación: A ≈ U
|
||||||
|
≡ ∀p relevante al procesamiento: p(A) = p(U)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
La experiencia de unidad no es mística. Es un estado del sistema donde los bordes entre "yo" y "mundo" dejan de filtrar. §VIII da métricas.
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Algoritmo de perdón:
|
||||||
|
for m in M_perturbadora:
|
||||||
|
1. anclar en P
|
||||||
|
2. reproducir m con R ≈ 0
|
||||||
|
3. peso_emocional(m) ← 0
|
||||||
|
4. if redundante(m): liberar(m)
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El perdón no es moral. Es un procedimiento de limpieza de memoria: tomar un recuerdo que duele, reproducirlo sin identificación, quitarle la carga, soltarlo si no sirve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VIII. Métrica
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Co = señal / ruido psicofisiológico
|
||||||
|
H = H(señal conductual) // entropía temporal
|
||||||
|
IR = t(baseline | perturbación)
|
||||||
|
PA = Δ(IR, Co) tras intervención
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Heurísticas para evaluar el sistema: ¿la atención fluye con menos esfuerzo? ¿El estado mental es menos errático? ¿Te recuperas más rápido? ¿Los cambios se mantienen?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IX. Protocolo
|
||||||
|
|
||||||
|
### Diario
|
||||||
|
|
||||||
|
- Reactividad (0-10)
|
||||||
|
- Duración del estado
|
||||||
|
- Intensidad (0-10)
|
||||||
|
|
||||||
|
### Escalonamiento
|
||||||
|
|
||||||
|
1. Detección
|
||||||
|
2. Pausa del script
|
||||||
|
3. Modulación
|
||||||
|
4. Consolidación
|
||||||
|
5. Mantenimiento
|
||||||
|
|
||||||
|
### Auditoría
|
||||||
|
|
||||||
|
Cada 4-12 semanas: Co, IR, H, PA. Sin mejora → ajustar protocolo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## X. Ontología
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Entidades: P, D, Cᵢ, R, T
|
||||||
|
Existencia: operativa (funcionan en el modelo)
|
||||||
|
Criterio de verdad: coherencia operativa
|
||||||
|
</code>
|
||||||
|
|
||||||
|
El modelo no afirma que la realidad sea así. Afirma que actuar como si fuera así mejora los resultados. Es una ontología instrumental.
|
||||||
|
|
||||||
|
Límites:
|
||||||
|
- Sesgos interpretativos y culturales en R
|
||||||
|
- Conocimiento probabilístico y parcial
|
||||||
|
- Validación fenomenológica co-requisito
|
||||||
|
|
||||||
|
<code>
|
||||||
|
Telos: max(Co) bajo restricciones energéticas
|
||||||
|
≡ mayor adaptabilidad + menor S + mayor U efectivo
|
||||||
|
</code>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## XI. Apéndice — modelo esquemático
|
||||||
|
|
||||||
|
<code>
|
||||||
|
ds/dt = -k·s - α·R(s) + I(t) + ε(t)
|
||||||
|
|
||||||
|
k > 0 amortiguamiento natural
|
||||||
|
R(s) = β·sⁿ (n ≥ 1) resistencia
|
||||||
|
α escala de resistencia
|
||||||
|
I(t) entrada externa
|
||||||
|
ε(t) ruido estocástico
|
||||||
|
|
||||||
|
Aceptación: β → 0
|
||||||
|
Modulación: k↑ o α↓
|
||||||
|
|
||||||
|
Regímenes:
|
||||||
|
Recaída: I alto + β alto → s divergente
|
||||||
|
Plasticidad: β→0, luego k alto consolida
|
||||||
|
Estable: R ≈ 0 → ds/dt ≈ -k·s + I + ε
|
||||||
|
</code>
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export function boot(): void;
|
||||||
|
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly boot: () => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_1466: (a: number, b: number, c: number, d: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_520: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_640: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_520_6: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_312: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wasm_bindgen_func_elem_313: (a: number, b: number) => void;
|
||||||
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
||||||
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
readonly __wbindgen_export3: (a: number) => void;
|
||||||
|
readonly __wbindgen_export4: (a: number, b: number) => void;
|
||||||
|
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
|
readonly __wbindgen_start: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the given `module`, which can either be bytes or
|
||||||
|
* a precompiled `WebAssembly.Module`.
|
||||||
|
*
|
||||||
|
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {InitOutput}
|
||||||
|
*/
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const boot: () => void;
|
||||||
|
export const __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_1466: (a: number, b: number, c: number, d: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_520: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_640: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_520_6: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_312: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wasm_bindgen_func_elem_313: (a: number, b: number) => void;
|
||||||
|
export const __wbindgen_export: (a: number, b: number) => number;
|
||||||
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
export const __wbindgen_export3: (a: number) => void;
|
||||||
|
export const __wbindgen_export4: (a: number, b: number) => void;
|
||||||
|
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
|
export const __wbindgen_start: () => void;
|
||||||
@@ -21,8 +21,9 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use barra_web::{Task, TaskList};
|
use barra_web::{Task, TaskList};
|
||||||
use gioser_canvas_web::{tips, Renderer};
|
use gioser_canvas_web::{tips, Renderer};
|
||||||
use pluma_reader_web::Reader;
|
use gioser_graph_web::GraphWidget;
|
||||||
use vista_web::Deck;
|
use fana_md_reader_web::Reader;
|
||||||
|
use revista_web::Deck;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
@@ -35,7 +36,7 @@ const TASKBAR_HEIGHT_PX: f32 = 52.0;
|
|||||||
const BUTTON_HALF_W_PX: f32 = 90.0;
|
const BUTTON_HALF_W_PX: f32 = 90.0;
|
||||||
const BUTTON_HALF_H_PX: f32 = 64.0;
|
const BUTTON_HALF_H_PX: f32 = 64.0;
|
||||||
const VIEWPORT_MARGIN_PX: f32 = 14.0;
|
const VIEWPORT_MARGIN_PX: f32 = 14.0;
|
||||||
const ELEMENTS: [&str; 4] = ["aire", "fuego", "tierra", "agua"];
|
const ELEMENTS: [&str; 9] = ["aire", "fuego", "tierra", "agua", "cuerpo", "sombra", "cosmos", "practica", "olvido"];
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct DeckState {
|
struct DeckState {
|
||||||
@@ -78,6 +79,17 @@ impl AppState {
|
|||||||
self.sync_active_class();
|
self.sync_active_class();
|
||||||
self.sync_taskbar();
|
self.sync_taskbar();
|
||||||
self.load_md_if_empty(element, md_url);
|
self.load_md_if_empty(element, md_url);
|
||||||
|
// Actualizar URL con history.pushState (sin #)
|
||||||
|
if let Some(win) = web_sys::window() {
|
||||||
|
if let Ok(hist) = win.history() {
|
||||||
|
let path = format!("/estudio/{}", element);
|
||||||
|
let _ = hist.push_state_with_url(
|
||||||
|
&wasm_bindgen::JsValue::NULL,
|
||||||
|
"",
|
||||||
|
Some(&path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_from_tab(&self, element: &str, origin_x: f64, origin_y: f64) {
|
fn restore_from_tab(&self, element: &str, origin_x: f64, origin_y: f64) {
|
||||||
@@ -105,6 +117,16 @@ impl AppState {
|
|||||||
self.sync_active_class();
|
self.sync_active_class();
|
||||||
self.sync_taskbar();
|
self.sync_taskbar();
|
||||||
self.hide_deck(origin_x, origin_y);
|
self.hide_deck(origin_x, origin_y);
|
||||||
|
// Restaurar URL
|
||||||
|
if let Some(win) = web_sys::window() {
|
||||||
|
if let Ok(hist) = win.history() {
|
||||||
|
let _ = hist.push_state_with_url(
|
||||||
|
&wasm_bindgen::JsValue::NULL,
|
||||||
|
"",
|
||||||
|
Some("/"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self, element: &str, origin_x: f64, origin_y: f64) {
|
fn close(&self, element: &str, origin_x: f64, origin_y: f64) {
|
||||||
@@ -155,6 +177,7 @@ impl AppState {
|
|||||||
if let Some(body) = self.document.body() {
|
if let Some(body) = self.document.body() {
|
||||||
let _ = body.class_list().add_1("deck-visible");
|
let _ = body.class_list().add_1("deck-visible");
|
||||||
}
|
}
|
||||||
|
self.sync_page_controls();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_deck(&self, x: f64, y: f64) {
|
fn hide_deck(&self, x: f64, y: f64) {
|
||||||
@@ -166,6 +189,18 @@ impl AppState {
|
|||||||
if let Some(body) = self.document.body() {
|
if let Some(body) = self.document.body() {
|
||||||
let _ = body.class_list().remove_1("deck-visible");
|
let _ = body.class_list().remove_1("deck-visible");
|
||||||
}
|
}
|
||||||
|
self.sync_page_controls();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_page_controls(&self) {
|
||||||
|
if let Some(ctl) = self.document.get_element_by_id("global-page-controls") {
|
||||||
|
let is_visible = self.state.borrow().active.is_some();
|
||||||
|
ctl.set_attribute("style", if is_visible {
|
||||||
|
"opacity:1;pointer-events:auto;"
|
||||||
|
} else {
|
||||||
|
"opacity:0;pointer-events:none;"
|
||||||
|
}).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deck_el(&self) -> Option<HtmlElement> {
|
fn deck_el(&self) -> Option<HtmlElement> {
|
||||||
@@ -230,16 +265,15 @@ impl AppState {
|
|||||||
"fuego" => ("Quién Soy", "Bitácora · Crónica"),
|
"fuego" => ("Quién Soy", "Bitácora · Crónica"),
|
||||||
"tierra" => ("Manifiesto", "Invariantes · Piedra de toque"),
|
"tierra" => ("Manifiesto", "Invariantes · Piedra de toque"),
|
||||||
"agua" => ("Mística", "Espiritualidad aplicada"),
|
"agua" => ("Mística", "Espiritualidad aplicada"),
|
||||||
|
"cuerpo" => ("El Cuerpo", "Somática · Respiración · Portal"),
|
||||||
|
"sombra" => ("La Sombra", "Integración · Patrones · Acecho"),
|
||||||
|
"cosmos" => ("Cosmovisión", "4 Elementos · Arquetipos"),
|
||||||
|
"practica" => ("Prácticas", "Ejercicios · Transformación"),
|
||||||
|
"olvido" => ("El Olvido", "Desaprender · Soltar · Vaciar"),
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let html = format!(
|
let html = format!(
|
||||||
"<article class=\"deck-page\" data-element=\"{el}\" id=\"deck-page-{el}\">\
|
"<article class=\"deck-page\" data-element=\"{el}\" id=\"deck-page-{el}\">\
|
||||||
<div class=\"page-controls\">\
|
|
||||||
<button class=\"page-control-btn page-minimize\" data-minimize=\"{el}\" type=\"button\" aria-label=\"Minimizar {title}\">\
|
|
||||||
<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 19 H19\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/></svg>\
|
|
||||||
</button>\
|
|
||||||
<button class=\"page-control-btn page-close\" data-close-page=\"{el}\" type=\"button\" aria-label=\"Cerrar {title}\">×</button>\
|
|
||||||
</div>\
|
|
||||||
<div class=\"page-ambience\" aria-hidden=\"true\"></div>\
|
<div class=\"page-ambience\" aria-hidden=\"true\"></div>\
|
||||||
<header class=\"page-head\">\
|
<header class=\"page-head\">\
|
||||||
<span class=\"page-mark\">{el}</span>\
|
<span class=\"page-mark\">{el}</span>\
|
||||||
@@ -272,13 +306,85 @@ impl AppState {
|
|||||||
if inner.contains("pluma-doc") {
|
if inner.contains("pluma-doc") {
|
||||||
return; // ya hidratado
|
return; // ya hidratado
|
||||||
}
|
}
|
||||||
let reader = Reader::new(content);
|
let document_clone = self.document.clone();
|
||||||
let element_owned = element.to_string();
|
let element_owned = element.to_string();
|
||||||
let url_owned = md_url.to_string();
|
let url_owned = md_url.to_string();
|
||||||
|
let reader = fana_md_reader_web::Reader::new(content.clone());
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let content_clone = content.clone();
|
||||||
if let Err(e) = reader.open_url(&url_owned, &element_owned).await {
|
if let Err(e) = reader.open_url(&url_owned, &element_owned).await {
|
||||||
web_sys::console::warn_1(&e);
|
web_sys::console::warn_1(&e);
|
||||||
}
|
}
|
||||||
|
// Después de cargar el md, montar el grafo debajo
|
||||||
|
let graph_container_id = format!("graph-{}-container", element_owned);
|
||||||
|
// Si ya existe, no lo duplicamos
|
||||||
|
if document_clone.get_element_by_id(&graph_container_id).is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Crear contenedor debajo del content
|
||||||
|
let wrapper: HtmlElement = document_clone
|
||||||
|
.create_element("div")
|
||||||
|
.ok()
|
||||||
|
.and_then(|e| e.dyn_into::<HtmlElement>().ok())
|
||||||
|
.unwrap();
|
||||||
|
wrapper.set_id(&graph_container_id);
|
||||||
|
wrapper.style().set_property("margin-top", "1rem").ok();
|
||||||
|
wrapper.style().set_property("padding-top", "1rem").ok();
|
||||||
|
wrapper.style().set_property("border-top", "1px solid rgba(255,255,255,0.08)").ok();
|
||||||
|
// Label
|
||||||
|
let label: HtmlElement = document_clone
|
||||||
|
.create_element("div")
|
||||||
|
.ok()
|
||||||
|
.and_then(|e| e.dyn_into::<HtmlElement>().ok())
|
||||||
|
.unwrap();
|
||||||
|
label.set_inner_html(
|
||||||
|
"<span style=\"font-family: Inter, sans-serif; font-size: 0.75rem; \
|
||||||
|
letter-spacing: 0.3em; text-transform: uppercase; color: rgba(232,234,245,0.45);\">
|
||||||
|
· grafo semántico ·
|
||||||
|
</span>"
|
||||||
|
);
|
||||||
|
wrapper.append_child(&label).ok();
|
||||||
|
content_clone.append_child(&wrapper).ok();
|
||||||
|
// Callback: recibe 'camino' del nodo clickeado y navega
|
||||||
|
let cb: Box<dyn FnMut(String)> = Box::new(move |target| {
|
||||||
|
web_sys::console::log_1(&format!("DEBUG grafo: click target={}", target).into());
|
||||||
|
// Mapa: camino → id del tip en HTML
|
||||||
|
let el = match target.as_str() {
|
||||||
|
"logos" | "aire" => "aire",
|
||||||
|
"nomos" | "fuego" => "fuego",
|
||||||
|
"kay" | "tierra" => "tierra",
|
||||||
|
"uku" | "agua" => "agua",
|
||||||
|
"cuerpo" => "cuerpo",
|
||||||
|
"sombra" => "sombra",
|
||||||
|
"cosmos" => "cosmos",
|
||||||
|
"practica" => "practica",
|
||||||
|
"olvido" => "olvido",
|
||||||
|
_ => "aire",
|
||||||
|
};
|
||||||
|
web_sys::console::log_1(&format!("DEBUG grafo: el={}", el).into());
|
||||||
|
let sel = format!(".tip[data-md][id='tip-{}']", el);
|
||||||
|
web_sys::console::log_1(&format!("DEBUG grafo: selector={}", sel).into());
|
||||||
|
match document_clone.query_selector(&sel).ok().flatten() {
|
||||||
|
Some(tip) => {
|
||||||
|
web_sys::console::log_1(&"DEBUG grafo: tip encontrado, llamando click()".into());
|
||||||
|
let tip_html: HtmlElement = tip.clone().dyn_into().unwrap();
|
||||||
|
tip_html.click();
|
||||||
|
web_sys::console::log_1(&"DEBUG grafo: click() ejecutado".into());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
web_sys::console::log_1(&"DEBUG grafo: tip NO encontrado".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut graph = GraphWidget::new(
|
||||||
|
wrapper,
|
||||||
|
"https://api.gioser.net",
|
||||||
|
Some(cb),
|
||||||
|
);
|
||||||
|
if let Err(e) = graph.load().await {
|
||||||
|
web_sys::console::warn_1(&format!("grafo: error al cargar: {:?}", e).into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,11 +480,51 @@ pub fn boot() -> Result<(), JsValue> {
|
|||||||
install_canvas_pointer(&canvas, &renderer)?;
|
install_canvas_pointer(&canvas, &renderer)?;
|
||||||
install_canvas_leave(&canvas, &renderer)?;
|
install_canvas_leave(&canvas, &renderer)?;
|
||||||
install_tip_clicks(&document, &app)?;
|
install_tip_clicks(&document, &app)?;
|
||||||
install_deck_delegation(&document, &app)?;
|
install_controls_delegation(&document, &app)?;
|
||||||
install_taskbar(&document, &app)?;
|
install_taskbar(&document, &app)?;
|
||||||
install_keyboard(&document, &app)?;
|
install_keyboard(&document, &app)?;
|
||||||
|
install_popstate_listener(&window, &app)?;
|
||||||
install_raf(&window, &document, &canvas, &renderer);
|
install_raf(&window, &document, &canvas, &renderer);
|
||||||
|
|
||||||
|
// Leer ruta inicial para abrir página directa
|
||||||
|
if let Ok(pathname) = window.location().pathname() {
|
||||||
|
let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
|
||||||
|
if !clean.is_empty() {
|
||||||
|
if let Some(el) = document.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
|
||||||
|
let rect = el.get_bounding_client_rect();
|
||||||
|
let cx = rect.left() + rect.width() / 2.0;
|
||||||
|
let cy = rect.top() + rect.height() / 2.0;
|
||||||
|
if let Some(md_url) = el.get_attribute("data-md") {
|
||||||
|
app.open_or_switch(clean, cx, cy, &md_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_popstate_listener(window: &Window, app: &Rc<AppState>) -> Result<(), JsValue> {
|
||||||
|
let app2 = app.clone();
|
||||||
|
let doc = app.document.clone();
|
||||||
|
let win2 = window.clone();
|
||||||
|
let cb = Closure::<dyn FnMut(Event)>::new(move |_e: Event| {
|
||||||
|
if let Ok(pathname) = win2.location().pathname() {
|
||||||
|
let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
|
||||||
|
if clean.is_empty() || clean == "/" {
|
||||||
|
app2.home();
|
||||||
|
} else if let Some(el) = doc.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
|
||||||
|
let rect = el.get_bounding_client_rect();
|
||||||
|
let cx = rect.left() + rect.width() / 2.0;
|
||||||
|
let cy = rect.top() + rect.height() / 2.0;
|
||||||
|
if let Some(md_url) = el.get_attribute("data-md") {
|
||||||
|
app2.open_or_switch(clean, cx, cy, &md_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.add_event_listener_with_callback("popstate", cb.as_ref().unchecked_ref())?;
|
||||||
|
cb.forget();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,11 +620,14 @@ fn install_tip_clicks(document: &Document, app: &Rc<AppState>) -> Result<(), JsV
|
|||||||
let md_url = el.get_attribute("data-md").unwrap_or_default();
|
let md_url = el.get_attribute("data-md").unwrap_or_default();
|
||||||
let app2 = app.clone();
|
let app2 = app.clone();
|
||||||
let el_for_rect = el.clone();
|
let el_for_rect = el.clone();
|
||||||
|
let el_name = element.clone();
|
||||||
let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
|
let cb = Closure::<dyn FnMut(Event)>::new(move |e: Event| {
|
||||||
|
web_sys::console::log_1(&format!("DEBUG tip: click en {} isTrusted={}", el_name, e.is_trusted()).into());
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
let rect = el_for_rect.get_bounding_client_rect();
|
let rect = el_for_rect.get_bounding_client_rect();
|
||||||
let cx = rect.left() + rect.width() / 2.0;
|
let cx = rect.left() + rect.width() / 2.0;
|
||||||
let cy = rect.top() + rect.height() / 2.0;
|
let cy = rect.top() + rect.height() / 2.0;
|
||||||
|
web_sys::console::log_1(&format!("DEBUG tip: llamando open_or_switch({}, {}, {})", el_name, cx, cy).into());
|
||||||
app2.open_or_switch(&element, cx, cy, &md_url);
|
app2.open_or_switch(&element, cx, cy, &md_url);
|
||||||
});
|
});
|
||||||
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
|
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
|
||||||
@@ -490,10 +639,7 @@ fn install_tip_clicks(document: &Document, app: &Rc<AppState>) -> Result<(), JsV
|
|||||||
/// Un listener en el deck delega clicks de minimize y close en cada página.
|
/// Un listener en el deck delega clicks de minimize y close en cada página.
|
||||||
/// Las páginas se crean dinámicamente, así que no podemos adjuntar listeners
|
/// Las páginas se crean dinámicamente, así que no podemos adjuntar listeners
|
||||||
/// por botón en boot.
|
/// por botón en boot.
|
||||||
fn install_deck_delegation(document: &Document, app: &Rc<AppState>) -> Result<(), JsValue> {
|
fn install_controls_delegation(document: &Document, app: &Rc<AppState>) -> Result<(), JsValue> {
|
||||||
let Some(deck_el) = document.get_element_by_id("deck") else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let app2 = app.clone();
|
let app2 = app.clone();
|
||||||
let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
|
let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
|
||||||
let Some(target) = e.target() else { return };
|
let Some(target) = e.target() else { return };
|
||||||
@@ -504,25 +650,38 @@ fn install_deck_delegation(document: &Document, app: &Rc<AppState>) -> Result<()
|
|||||||
if let Ok(Some(btn)) = target_el.closest("[data-minimize]") {
|
if let Ok(Some(btn)) = target_el.closest("[data-minimize]") {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
let element = btn.get_attribute("data-minimize").unwrap_or_default();
|
let element = btn.get_attribute("data-minimize").unwrap_or_default();
|
||||||
// Origin = la cajita correspondiente en la taskbar (efecto
|
// Si el data-minimize está vacío, usar el elemento activo
|
||||||
// visual: la página se "encoge" hacia su entrada del taskbar).
|
let el = if element.is_empty() {
|
||||||
|
app2.state.borrow().active.clone().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
element
|
||||||
|
};
|
||||||
|
if !el.is_empty() {
|
||||||
let origin = app2
|
let origin = app2
|
||||||
.taskbar_item_center(&element)
|
.taskbar_item_center(&el)
|
||||||
.unwrap_or_else(|| center_of_element(&btn));
|
.unwrap_or_else(|| center_of_element(&btn));
|
||||||
app2.minimize(origin.0, origin.1);
|
app2.minimize(origin.0, origin.1);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Close
|
// Close
|
||||||
if let Ok(Some(btn)) = target_el.closest("[data-close-page]") {
|
if let Ok(Some(btn)) = target_el.closest("[data-close-page]") {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
let element = btn.get_attribute("data-close-page").unwrap_or_default();
|
let element = btn.get_attribute("data-close-page").unwrap_or_default();
|
||||||
|
let el = if element.is_empty() {
|
||||||
|
app2.state.borrow().active.clone().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
element
|
||||||
|
};
|
||||||
|
if !el.is_empty() {
|
||||||
let origin = app2
|
let origin = app2
|
||||||
.taskbar_item_center(&element)
|
.taskbar_item_center(&el)
|
||||||
.unwrap_or_else(|| center_of_element(&btn));
|
.unwrap_or_else(|| center_of_element(&btn));
|
||||||
app2.close(&element, origin.0, origin.1);
|
app2.close(&el, origin.0, origin.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
deck_el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
|
document.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
|
||||||
cb.forget();
|
cb.forget();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -639,6 +798,35 @@ fn position_tips(document: &Document, canvas: &HtmlCanvasElement, renderer: &Ren
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mapea un doc_id de Qdrant al nombre del elemento (aire/fuego/tierra/agua)
|
||||||
|
/// y su ruta md. Los doc_ids se generan con uuid5 en el indexador, pero
|
||||||
|
/// podemos inferir por el nombre del camino o del elemento.
|
||||||
|
fn map_doc_id_to_element(doc_id: &str) -> (String, String) {
|
||||||
|
// Inferir del doc_id: contiene el nombre del elemento
|
||||||
|
let el = if doc_id.contains("aire") || doc_id.contains("logos") {
|
||||||
|
"aire"
|
||||||
|
} else if doc_id.contains("fuego") || doc_id.contains("nomos") {
|
||||||
|
"fuego"
|
||||||
|
} else if doc_id.contains("tierra") || doc_id.contains("kay") {
|
||||||
|
"tierra"
|
||||||
|
} else if doc_id.contains("agua") || doc_id.contains("uku") {
|
||||||
|
"agua"
|
||||||
|
} else if doc_id.contains("cuerpo") {
|
||||||
|
"cuerpo"
|
||||||
|
} else if doc_id.contains("sombra") {
|
||||||
|
"sombra"
|
||||||
|
} else if doc_id.contains("cosmos") {
|
||||||
|
"cosmos"
|
||||||
|
} else if doc_id.contains("practica") {
|
||||||
|
"practica"
|
||||||
|
} else if doc_id.contains("olvido") {
|
||||||
|
"olvido"
|
||||||
|
} else {
|
||||||
|
"aire"
|
||||||
|
};
|
||||||
|
(el.to_string(), format!("./md/{}.md", el))
|
||||||
|
}
|
||||||
|
|
||||||
fn install_panic_hook() {
|
fn install_panic_hook() {
|
||||||
static SET: std::sync::Once = std::sync::Once::new();
|
static SET: std::sync::Once = std::sync::Once::new();
|
||||||
SET.call_once(|| {
|
SET.call_once(|| {
|
||||||
|
|||||||
+378
-590
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,679 @@
|
|||||||
|
/* === Tokens === */
|
||||||
|
:root {
|
||||||
|
--bg: #06050d;
|
||||||
|
--fg: #e8eaf5;
|
||||||
|
--gold: #d8a85d;
|
||||||
|
--gold-deep: #b77e34;
|
||||||
|
--aire: #d0dbff;
|
||||||
|
--agua: #6cd0f3;
|
||||||
|
--fuego: #f59056;
|
||||||
|
--tierra: #d49873;
|
||||||
|
--cuerpo: #e07a5f;
|
||||||
|
--sombra: #4a4a5a;
|
||||||
|
--cosmos: #d4a843;
|
||||||
|
--practica: #2d936c;
|
||||||
|
--olvido: #b0b8c0;
|
||||||
|
|
||||||
|
--ease-emerge: cubic-bezier(0.22, 0.61, 0.20, 1);
|
||||||
|
--ease-magma: cubic-bezier(0.32, 0, 0.05, 1);
|
||||||
|
--ease-page: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||||
|
|
||||||
|
--taskbar-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Canvas WebGL === */
|
||||||
|
#gioser-canvas {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
z-index: 0;
|
||||||
|
transition: opacity 600ms var(--ease-emerge), filter 600ms var(--ease-emerge);
|
||||||
|
}
|
||||||
|
body.deck-visible #gioser-canvas {
|
||||||
|
opacity: 0.30;
|
||||||
|
filter: blur(4px) saturate(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tips (botones cardinales sobre el aro) === */
|
||||||
|
#tips {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
transition: opacity 250ms ease;
|
||||||
|
}
|
||||||
|
body.deck-visible #tips {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 1.1rem 1.6rem 0.95rem;
|
||||||
|
min-width: 168px;
|
||||||
|
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.04), transparent 65%),
|
||||||
|
rgba(8, 6, 22, 0.55);
|
||||||
|
backdrop-filter: blur(12px) saturate(140%);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(140%);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(216, 168, 93, 0.25);
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 12px 36px rgba(0, 0, 0, 0.40),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
|
transition:
|
||||||
|
box-shadow 350ms var(--ease-emerge),
|
||||||
|
border-color 350ms var(--ease-emerge),
|
||||||
|
background 350ms var(--ease-emerge);
|
||||||
|
}
|
||||||
|
.tip::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -10px;
|
||||||
|
border-radius: 26px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 400ms ease, inset 400ms var(--ease-emerge);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tip:hover {
|
||||||
|
border-color: currentColor;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at top, rgba(255, 255, 255, 0.07), transparent 65%),
|
||||||
|
rgba(20, 14, 40, 0.72);
|
||||||
|
}
|
||||||
|
.tip:hover::before {
|
||||||
|
opacity: 0.45;
|
||||||
|
inset: -16px;
|
||||||
|
}
|
||||||
|
.tip-glyph {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
color: currentColor;
|
||||||
|
filter: drop-shadow(0 0 6px currentColor) drop-shadow(0 0 16px currentColor);
|
||||||
|
transition: filter 320ms ease, transform 350ms var(--ease-emerge);
|
||||||
|
}
|
||||||
|
.tip:hover .tip-glyph {
|
||||||
|
filter: drop-shadow(0 0 14px currentColor) drop-shadow(0 0 28px currentColor);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
.tip-label {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.42em;
|
||||||
|
font-weight: 600;
|
||||||
|
text-indent: 0.42em;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
.tip-sub {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
font-weight: 300;
|
||||||
|
color: rgba(232, 234, 245, 0.62);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-indent: 0.22em;
|
||||||
|
}
|
||||||
|
.tip-aire { color: var(--aire); }
|
||||||
|
.tip-fuego { color: var(--fuego); }
|
||||||
|
.tip-agua { color: var(--agua); }
|
||||||
|
.tip-tierra { color: var(--tierra); }
|
||||||
|
.tip-cuerpo { color: var(--cuerpo); }
|
||||||
|
.tip-sombra { color: var(--sombra); }
|
||||||
|
.tip-cosmos { color: var(--cosmos); }
|
||||||
|
.tip-practica { color: var(--practica); }
|
||||||
|
.tip-olvido { color: var(--olvido); }
|
||||||
|
|
||||||
|
/* === DECK: contenedor único de páginas swipeable === */
|
||||||
|
.deck {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--taskbar-height);
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
|
||||||
|
transform: scale(0.0);
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at center, var(--deck-glow, rgba(216, 168, 93, 0.15)), transparent 65%),
|
||||||
|
rgba(6, 5, 13, 0.96);
|
||||||
|
backdrop-filter: blur(28px) saturate(140%);
|
||||||
|
-webkit-backdrop-filter: blur(28px) saturate(140%);
|
||||||
|
transition:
|
||||||
|
transform 600ms var(--ease-magma),
|
||||||
|
opacity 450ms ease,
|
||||||
|
visibility 0s 600ms;
|
||||||
|
}
|
||||||
|
.deck.open {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: scale(1);
|
||||||
|
transition:
|
||||||
|
transform 600ms var(--ease-magma),
|
||||||
|
opacity 450ms ease,
|
||||||
|
visibility 0s;
|
||||||
|
}
|
||||||
|
/* Acento del deck según elemento activo: glow radial del color. */
|
||||||
|
body.deck-active-aire .deck { --deck-glow: rgba(208, 219, 255, 0.22); }
|
||||||
|
body.deck-active-fuego .deck { --deck-glow: rgba(245, 144, 86, 0.28); }
|
||||||
|
body.deck-active-agua .deck { --deck-glow: rgba(108, 208, 243, 0.22); }
|
||||||
|
body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
|
||||||
|
|
||||||
|
/* Strip horizontal con páginas — vista-web traslada esto.
|
||||||
|
touch-action: pan-y declara al browser "yo manejo horizontal, el
|
||||||
|
vertical (scroll interno de cada página) lo dejas pasar". Sin esto
|
||||||
|
el navegador móvil se traga el gesto horizontal antes de que JS
|
||||||
|
pueda capturarlo. */
|
||||||
|
.deck-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: translate3d(var(--vista-offset, 0px), 0, 0);
|
||||||
|
transition: transform 360ms var(--ease-page);
|
||||||
|
will-change: transform;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
/* Asegurar que TODOS los descendientes del strip hereden el contrato
|
||||||
|
touch-action — si el toque llega a un párrafo o <a>, el browser
|
||||||
|
chequea el touch-action del target, no del padre. */
|
||||||
|
.deck-strip * {
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
.deck-strip.vista-dragging,
|
||||||
|
.deck-strip.vista-instant {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-page {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
.deck-page[data-element="aire"] { --page-accent: var(--aire); }
|
||||||
|
.deck-page[data-element="fuego"] { --page-accent: var(--fuego); }
|
||||||
|
.deck-page[data-element="agua"] { --page-accent: var(--agua); }
|
||||||
|
.deck-page[data-element="tierra"] { --page-accent: var(--tierra); }
|
||||||
|
|
||||||
|
/* Ambience por página */
|
||||||
|
.page-ambience {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
transition: opacity 2s ease;
|
||||||
|
filter: blur(60px);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.deck-page[data-element="aire"] .page-ambience {
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(208,219,255,0.22), transparent 60%);
|
||||||
|
animation: page-breathe 8s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
.deck-page[data-element="fuego"] .page-ambience {
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(245,144,86,0.22), transparent 60%);
|
||||||
|
animation: page-breathe 6s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
.deck-page[data-element="agua"] .page-ambience {
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(108,208,243,0.22), transparent 60%);
|
||||||
|
animation: page-breathe 10s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
.deck-page[data-element="tierra"] .page-ambience {
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(140,100,60,0.22), transparent 60%);
|
||||||
|
animation: page-breathe 7s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes page-breathe {
|
||||||
|
from { opacity: 0.30; }
|
||||||
|
to { opacity: 0.60; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Head + controls — fijos en el deck, no dentro de la página */
|
||||||
|
.page-controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 1.2rem;
|
||||||
|
right: 1.2rem;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
body.deck-visible .page-controls {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.page-control-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
color: var(--page-accent, var(--gold));
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
transition:
|
||||||
|
background 200ms ease,
|
||||||
|
border-color 200ms ease,
|
||||||
|
transform 250ms var(--ease-emerge);
|
||||||
|
}
|
||||||
|
.page-control-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.10);
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
.page-minimize svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.page-close {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
.page-close:hover { transform: rotate(90deg); }
|
||||||
|
|
||||||
|
.page-head {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 7vh 8vw 2vh;
|
||||||
|
color: var(--page-accent, var(--gold));
|
||||||
|
}
|
||||||
|
.page-mark {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.55em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(232, 234, 245, 0.6);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
text-indent: 0.55em;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(2.4rem, 6vw, 4.6rem);
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--page-accent, var(--gold));
|
||||||
|
text-shadow: 0 0 28px currentColor, 0 0 56px rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
.page-tag {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.32em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(232, 234, 245, 0.55);
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
text-indent: 0.32em;
|
||||||
|
}
|
||||||
|
.page-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 1vh 10vw 8vh;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 400ms ease 250ms;
|
||||||
|
}
|
||||||
|
.deck.open .deck-page .page-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === pluma-doc dentro de la página === */
|
||||||
|
.pluma-doc {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.78;
|
||||||
|
color: rgba(232, 234, 245, 0.92);
|
||||||
|
}
|
||||||
|
.pluma-doc > * + * { margin-top: 1.0em; }
|
||||||
|
.pluma-doc h1 {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: clamp(1.7rem, 3vw, 2.4rem);
|
||||||
|
color: var(--page-accent);
|
||||||
|
text-shadow: 0 0 18px currentColor;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-top: 1.4em;
|
||||||
|
}
|
||||||
|
.pluma-doc h2 {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--page-accent);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
.pluma-doc h3 {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.18rem;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
.pluma-doc p { margin: 0; }
|
||||||
|
.pluma-doc a {
|
||||||
|
color: var(--page-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
.pluma-doc a:hover { opacity: 0.7; }
|
||||||
|
.pluma-doc strong { color: rgba(255, 255, 255, 0.98); font-weight: 600; }
|
||||||
|
.pluma-doc em { color: rgba(255, 255, 255, 0.92); }
|
||||||
|
.pluma-doc code {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
padding: 0.12em 0.45em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.92em;
|
||||||
|
color: var(--page-accent);
|
||||||
|
}
|
||||||
|
.pluma-doc pre {
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-left: 3px solid var(--page-accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
.pluma-doc pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(232, 234, 245, 0.92);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.pluma-doc blockquote {
|
||||||
|
border-left: 3px solid var(--page-accent);
|
||||||
|
padding: 0.4em 1.2em;
|
||||||
|
color: rgba(232, 234, 245, 0.75);
|
||||||
|
font-style: italic;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
}
|
||||||
|
.pluma-doc ul, .pluma-doc ol { padding-left: 1.6em; }
|
||||||
|
.pluma-doc li { margin: 0.4em 0; }
|
||||||
|
.pluma-doc li::marker { color: var(--page-accent); }
|
||||||
|
.pluma-doc hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, transparent, var(--page-accent), transparent);
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
.pluma-doc table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.pluma-doc th, .pluma-doc td {
|
||||||
|
padding: 0.55em 0.9em;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.pluma-doc th {
|
||||||
|
color: var(--page-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.pluma-loading, .pluma-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 30vh;
|
||||||
|
color: rgba(232, 234, 245, 0.55);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.pluma-loading::before {
|
||||||
|
content: "";
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
border: 1px solid var(--page-accent);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pluma-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
.pluma-error { color: var(--fuego); font-style: italic; }
|
||||||
|
@keyframes pluma-spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* === Taskbar estilo Windows === */
|
||||||
|
.taskbar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: var(--taskbar-height);
|
||||||
|
z-index: 200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
background: linear-gradient(to top, rgba(6, 5, 13, 0.94), rgba(8, 6, 22, 0.80));
|
||||||
|
backdrop-filter: blur(24px) saturate(140%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(140%);
|
||||||
|
border-top: 1px solid rgba(216, 168, 93, 0.22);
|
||||||
|
box-shadow: 0 -10px 36px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-home {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(216, 168, 93, 0.32);
|
||||||
|
color: var(--gold);
|
||||||
|
border-radius: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 220ms var(--ease-emerge);
|
||||||
|
}
|
||||||
|
.taskbar-home:hover {
|
||||||
|
border-color: var(--gold);
|
||||||
|
background: rgba(216, 168, 93, 0.12);
|
||||||
|
box-shadow: 0 0 16px rgba(216, 168, 93, 0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.taskbar-home-glyph {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
filter: drop-shadow(0 0 6px currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-brand {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.30rem;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #f4eedf;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 0 14px rgba(216, 168, 93, 0.45);
|
||||||
|
padding: 0 0.55rem;
|
||||||
|
user-select: none;
|
||||||
|
transition: text-shadow 220ms ease, color 220ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.taskbar-brand:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 0 20px rgba(216, 168, 93, 0.7), 0 0 36px rgba(245, 144, 86, 0.30);
|
||||||
|
}
|
||||||
|
.taskbar-brand .brand-dot {
|
||||||
|
color: var(--gold);
|
||||||
|
margin: 0 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-divider {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 26px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.taskbar-list::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.taskbar-item {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 1.0rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||||
|
border-radius: 9px;
|
||||||
|
color: var(--task-color, var(--fg));
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.36em;
|
||||||
|
text-indent: 0.36em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 260ms var(--ease-emerge);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.taskbar-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
border-color: var(--task-color, currentColor);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.taskbar-item.active {
|
||||||
|
background: rgba(255, 255, 255, 0.11);
|
||||||
|
border-color: var(--task-color);
|
||||||
|
box-shadow:
|
||||||
|
0 0 18px var(--task-color),
|
||||||
|
inset 0 -2px 0 0 var(--task-color);
|
||||||
|
}
|
||||||
|
.taskbar-item-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--task-color, var(--gold));
|
||||||
|
box-shadow: 0 0 8px currentColor;
|
||||||
|
}
|
||||||
|
.taskbar-item[data-task="aire"] { --task-color: var(--aire); }
|
||||||
|
.taskbar-item[data-task="fuego"] { --task-color: var(--fuego); }
|
||||||
|
.taskbar-item[data-task="tierra"] { --task-color: var(--tierra); }
|
||||||
|
.taskbar-item[data-task="agua"] { --task-color: var(--agua); }
|
||||||
|
|
||||||
|
.taskbar-spacer {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskbar-credit {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: rgba(216, 168, 93, 0.75);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 220ms var(--ease-emerge);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.taskbar-credit:hover {
|
||||||
|
color: var(--gold);
|
||||||
|
border-color: rgba(216, 168, 93, 0.25);
|
||||||
|
background: rgba(216, 168, 93, 0.06);
|
||||||
|
}
|
||||||
|
.copyleft-mark {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1rem;
|
||||||
|
/* © con escala horizontal -1 = copyleft visual. */
|
||||||
|
transform: scaleX(-1);
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.tip { min-width: 110px; padding: 0.7rem 0.9rem; }
|
||||||
|
.tip-glyph { width: 36px; height: 36px; }
|
||||||
|
.tip-label { font-size: 0.72rem; }
|
||||||
|
.tip-sub { display: none; }
|
||||||
|
.page-head { padding: 5vh 5vw 1vh; }
|
||||||
|
.page-content { padding: 0 5vw 5vh; }
|
||||||
|
.taskbar { height: 46px; padding: 0 0.4rem; gap: 0.3rem; }
|
||||||
|
.taskbar-home { width: 36px; height: 36px; }
|
||||||
|
.taskbar-item { height: 34px; padding: 0 0.7rem; font-size: 0.65rem; }
|
||||||
|
.taskbar-brand { font-size: 1.05rem; padding: 0 0.3rem; }
|
||||||
|
.taskbar-credit-text { display: none; }
|
||||||
|
.deck { bottom: 46px; }
|
||||||
|
:root { --taskbar-height: 46px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Los tips nuevos (cuerpo, sombra, cosmos, practica, olvido) existen
|
||||||
|
en el DOM para navegación JS (popstate, grafo), pero no se ven en
|
||||||
|
la chacana — no hay canvas-glyph para ellos. Se ocultan por defecto. */
|
||||||
|
.tip-hidden {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "lapaloma-demo"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
authors = { workspace = true }
|
|
||||||
publish = { workspace = true }
|
|
||||||
description = "Lapaloma — demo app: una serie sin(x) sobre ChartViewport rendereada con LapalomaChartElement. Valida la cadena core → render → cartesian → gpui en vivo."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gpui = { workspace = true }
|
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
|
||||||
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
|
|
||||||
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
|
|
||||||
lapaloma-cartesian = { path = "../../modules/ui_engine/widgets/lapaloma-cartesian" }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "lapaloma-financial-demo"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
authors = { workspace = true }
|
|
||||||
publish = { workspace = true }
|
|
||||||
description = "Lapaloma — demo de candlesticks OHLC. Random walk sintético de 120 días con pan + zoom."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gpui = { workspace = true }
|
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
|
||||||
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
|
|
||||||
lapaloma-cartesian = { path = "../../modules/ui_engine/widgets/lapaloma-cartesian" }
|
|
||||||
lapaloma-financial = { path = "../../modules/ui_engine/widgets/lapaloma-financial" }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "lapaloma-phosphor-demo"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
authors = { workspace = true }
|
|
||||||
publish = { workspace = true }
|
|
||||||
description = "Lapaloma — demo del trail CRT (phosphor) sobre un RingBuffer streaming a 60Hz. Compará con lapaloma-stream-demo para ver el contraste."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gpui = { workspace = true }
|
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
|
||||||
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
|
|
||||||
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
|
|
||||||
lapaloma-phosphor = { path = "../../modules/ui_engine/widgets/lapaloma-phosphor" }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "lapaloma-stream-demo"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
authors = { workspace = true }
|
|
||||||
publish = { workspace = true }
|
|
||||||
description = "Lapaloma — demo de streaming: RingBuffer + timer 60 Hz + sweep render. Showcase del zero-alloc en hot path."
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gpui = { workspace = true }
|
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
|
||||||
lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
|
|
||||||
lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
|
|
||||||
lapaloma-stream = { path = "../../modules/ui_engine/widgets/lapaloma-stream" }
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "matilda"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "matilda — CLI de administración de servidores: carga un inventario, muestra el plan, emite el script y lo aplica (local, remoto por SSH, o en seco)."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "matilda"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
matilda-core = { path = "../../modules/matilda/matilda-core" }
|
||||||
|
matilda-config = { path = "../../modules/matilda/matilda-config" }
|
||||||
|
matilda-plan = { path = "../../modules/matilda/matilda-plan" }
|
||||||
|
matilda-apply = { path = "../../modules/matilda/matilda-apply" }
|
||||||
|
matilda-ghost = { path = "../../modules/matilda/matilda-ghost" }
|
||||||
|
matilda-linker = { path = "../../modules/matilda/matilda-linker" }
|
||||||
|
matilda-discover = { path = "../../modules/matilda/matilda-discover" }
|
||||||
|
clap = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
//! `matilda` — CLI de administración de servidores.
|
||||||
|
//!
|
||||||
|
//! Carga un inventario declarativo (JSON), lo reconcilia contra el
|
||||||
|
//! estado actual y aplica los cambios — localmente, en seco, o en un
|
||||||
|
//! servidor remoto por SSH:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! matilda example imprime un inventario de ejemplo
|
||||||
|
//! matilda plan inv.json muestra el plan de reconciliación
|
||||||
|
//! matilda script inv.json emite el script de aplicación
|
||||||
|
//! matilda apply inv.json aplica localmente
|
||||||
|
//! matilda apply inv.json --dry-run simula
|
||||||
|
//! matilda apply inv.json --host deploy@srv aplica por SSH
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use matilda_apply::{plan_to_steps, steps_to_script, ApplyStep};
|
||||||
|
use matilda_core::{Container, Host, Inventory, RestartPolicy, VHost};
|
||||||
|
use matilda_ghost::ApplyReport;
|
||||||
|
use matilda_linker::{Linker, SshAuth, SshConfig};
|
||||||
|
use matilda_plan::{plan, Op};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "matilda", about = "Administración declarativa de servidores")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Cmd {
|
||||||
|
/// Imprime un inventario de ejemplo para editar.
|
||||||
|
Example,
|
||||||
|
/// Muestra el plan de reconciliación del inventario.
|
||||||
|
Plan {
|
||||||
|
inventory: PathBuf,
|
||||||
|
/// Estado actual del servidor (por defecto: vacío).
|
||||||
|
#[arg(long)]
|
||||||
|
current: Option<PathBuf>,
|
||||||
|
/// Descubre el estado actual de esta máquina (docker + nginx).
|
||||||
|
#[arg(long)]
|
||||||
|
discover: bool,
|
||||||
|
},
|
||||||
|
/// Emite el script de shell que aplicaría el plan.
|
||||||
|
Script {
|
||||||
|
inventory: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
current: Option<PathBuf>,
|
||||||
|
#[arg(long)]
|
||||||
|
discover: bool,
|
||||||
|
},
|
||||||
|
/// Aplica el plan: local, en seco, o remoto por SSH.
|
||||||
|
Apply {
|
||||||
|
inventory: PathBuf,
|
||||||
|
#[arg(long)]
|
||||||
|
current: Option<PathBuf>,
|
||||||
|
/// Descubre el estado actual de esta máquina antes de reconciliar.
|
||||||
|
#[arg(long)]
|
||||||
|
discover: bool,
|
||||||
|
/// Simula sin tocar nada.
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
/// Aplica en un host remoto, `usuario@host`.
|
||||||
|
#[arg(long)]
|
||||||
|
host: Option<String>,
|
||||||
|
/// Contraseña SSH (si no se da, se usa la clave por defecto).
|
||||||
|
#[arg(long)]
|
||||||
|
password: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carga un inventario JSON desde un archivo.
|
||||||
|
fn load(path: &PathBuf) -> Result<Inventory, String> {
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
|
.map_err(|e| format!("no se pudo leer {}: {e}", path.display()))?;
|
||||||
|
serde_json::from_str(&text).map_err(|e| format!("JSON inválido en {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resuelve el inventario "actual" contra el que reconciliar:
|
||||||
|
/// `--discover` observa esta máquina; `--current` lee un archivo; si no,
|
||||||
|
/// se parte de un inventario vacío (todo es creación).
|
||||||
|
fn current_inventory(
|
||||||
|
discover: bool,
|
||||||
|
current: &Option<PathBuf>,
|
||||||
|
desired: &Inventory,
|
||||||
|
) -> Result<Inventory, String> {
|
||||||
|
if discover {
|
||||||
|
// Descubrimiento detallado: `docker inspect` detecta el drift.
|
||||||
|
Ok(matilda_discover::discover_inventory(desired))
|
||||||
|
} else {
|
||||||
|
match current {
|
||||||
|
Some(p) => load(p),
|
||||||
|
None => Ok(Inventory::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye un inventario de ejemplo.
|
||||||
|
fn example_inventory() -> Inventory {
|
||||||
|
let mut inv = Inventory::new();
|
||||||
|
inv.add_host(Host::new("edge-1", "10.0.0.1").with_tag("prod"));
|
||||||
|
inv.add_container(
|
||||||
|
Container::new("web", "nginx:1.27")
|
||||||
|
.with_port(8080, 80)
|
||||||
|
.with_volume("/srv/site", "/usr/share/nginx/html")
|
||||||
|
.with_restart(RestartPolicy::Always),
|
||||||
|
);
|
||||||
|
inv.add_container(
|
||||||
|
Container::new("api", "ghcr.io/ejemplo/api:1.0")
|
||||||
|
.with_port(9000, 9000)
|
||||||
|
.with_env("DATABASE_URL", "postgres://db/app")
|
||||||
|
.with_restart(RestartPolicy::UnlessStopped),
|
||||||
|
);
|
||||||
|
inv.add_vhost(
|
||||||
|
VHost::to_container("sitio.com", "web", 80)
|
||||||
|
.with_alias("www.sitio.com")
|
||||||
|
.with_tls(),
|
||||||
|
);
|
||||||
|
inv
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imprime un `ApplyReport` legible.
|
||||||
|
fn print_report(report: &ApplyReport) {
|
||||||
|
for r in &report.results {
|
||||||
|
println!("\n{} {}", if r.ok { "✔" } else { "✘" }, r.describe);
|
||||||
|
for l in &r.log {
|
||||||
|
println!(" {l}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"\n{} de {} pasos aplicados.",
|
||||||
|
report.applied(),
|
||||||
|
report.results.len()
|
||||||
|
);
|
||||||
|
if !report.all_ok() {
|
||||||
|
println!("✘ se detuvo en el primer error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aplica los pasos en un host remoto por SSH.
|
||||||
|
async fn apply_remote(
|
||||||
|
target: &str,
|
||||||
|
password: Option<String>,
|
||||||
|
steps: &[ApplyStep],
|
||||||
|
) -> Result<ApplyReport, String> {
|
||||||
|
let (user, host) = target
|
||||||
|
.split_once('@')
|
||||||
|
.ok_or_else(|| format!("host inválido (esperaba usuario@host): {target}"))?;
|
||||||
|
let auth = match password {
|
||||||
|
Some(pw) => SshAuth::Password(pw),
|
||||||
|
None => {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
|
||||||
|
SshAuth::Key {
|
||||||
|
path: PathBuf::from(format!("{home}/.ssh/id_ed25519")),
|
||||||
|
passphrase: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let config = SshConfig::new(host, user, auth);
|
||||||
|
let linker = Linker::connect(&config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("conexión SSH: {e}"))?;
|
||||||
|
Ok(linker.apply(steps).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), String> {
|
||||||
|
match Cli::parse().cmd {
|
||||||
|
Cmd::Example => {
|
||||||
|
let json = serde_json::to_string_pretty(&example_inventory())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
println!("{json}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Cmd::Plan { inventory, current, discover } => {
|
||||||
|
let desired = load(&inventory)?;
|
||||||
|
let p = plan(¤t_inventory(discover, ¤t, &desired)?, &desired);
|
||||||
|
if p.is_empty() {
|
||||||
|
println!("Sin cambios: el servidor ya está al día.");
|
||||||
|
} else {
|
||||||
|
for (i, action) in p.actions.iter().enumerate() {
|
||||||
|
println!("{:>2}. {}", i + 1, action.describe());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"\n{} acciones — {} crear, {} actualizar, {} eliminar.",
|
||||||
|
p.len(),
|
||||||
|
p.count(Op::Create),
|
||||||
|
p.count(Op::Update),
|
||||||
|
p.count(Op::Remove),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cmd::Script { inventory, current, discover } => {
|
||||||
|
let desired = load(&inventory)?;
|
||||||
|
let p = plan(¤t_inventory(discover, ¤t, &desired)?, &desired);
|
||||||
|
print!("{}", steps_to_script(&plan_to_steps(&p, &desired)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cmd::Apply { inventory, current, discover, dry_run, host, password } => {
|
||||||
|
let desired = load(&inventory)?;
|
||||||
|
let p = plan(¤t_inventory(discover, ¤t, &desired)?, &desired);
|
||||||
|
let steps = plan_to_steps(&p, &desired);
|
||||||
|
if steps.is_empty() {
|
||||||
|
println!("Sin cambios: nada que aplicar.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let report = if dry_run {
|
||||||
|
println!("— simulación (no se toca nada) —");
|
||||||
|
matilda_ghost::dry_run(&steps)
|
||||||
|
} else if let Some(target) = host {
|
||||||
|
println!("— aplicando en {target} por SSH —");
|
||||||
|
let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
rt.block_on(apply_remote(&target, password, &steps))?
|
||||||
|
} else {
|
||||||
|
println!("— aplicando localmente —");
|
||||||
|
matilda_ghost::apply(&steps)
|
||||||
|
};
|
||||||
|
print_report(&report);
|
||||||
|
if !report.all_ok() {
|
||||||
|
return Err("la aplicación falló".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match run() {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ description = "Dashboard GPUI del repo Minga: counts de nodos AST, atestaciones,
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
minga-store = { path = "../../modules/semantic_dht/minga-store" }
|
minga-store = { path = "../../modules/semantic_dht/minga-store" }
|
||||||
yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
|
nahual-widget-banner = { path = "../../modules/nahual/widgets/banner" }
|
||||||
yahweh-widget-stat-card = { path = "../../modules/ui_engine/widgets/stat-card" }
|
nahual-widget-stat-card = { path = "../../modules/nahual/widgets/stat-card" }
|
||||||
yahweh-widget-app-header = { path = "../../modules/ui_engine/widgets/app-header" }
|
nahual-widget-app-header = { path = "../../modules/nahual/widgets/app-header" }
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
//! (`minga status`) cuando hace falta el DID. El explorer foco es
|
//! (`minga status`) cuando hace falta el DID. El explorer foco es
|
||||||
//! observabilidad rápida.
|
//! observabilidad rápida.
|
||||||
//!
|
//!
|
||||||
//! Stack visual: yahweh-theme + banner_themed + card_themed +
|
//! Stack visual: nahual-theme + banner_themed + card_themed +
|
||||||
//! theme_switcher. Mismo patrón que `nakui-explorer` /
|
//! theme_switcher. Mismo patrón que `nakui-explorer` /
|
||||||
//! `nouser-explorer`.
|
//! `chasqui-explorer`.
|
||||||
//!
|
//!
|
||||||
//! Uso:
|
//! Uso:
|
||||||
//! ```sh
|
//! ```sh
|
||||||
@@ -30,11 +30,11 @@ use gpui::{
|
|||||||
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
div, prelude::*, px, Context, IntoElement, Render, SharedString, Window,
|
||||||
};
|
};
|
||||||
use minga_store::PersistentRepo;
|
use minga_store::PersistentRepo;
|
||||||
use yahweh_launcher::launch_app;
|
use nahual_launcher::launch_app;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_app_header::app_header;
|
use nahual_widget_app_header::app_header;
|
||||||
use yahweh_widget_banner::{banner_themed, Banner};
|
use nahual_widget_banner::{banner_themed, Banner};
|
||||||
use yahweh_widget_stat_card::stat_card;
|
use nahual_widget_stat_card::stat_card;
|
||||||
|
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
const REPO_DIRNAME: &str = "repo";
|
const REPO_DIRNAME: &str = "repo";
|
||||||
@@ -288,7 +288,7 @@ impl Render for Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// `stat_card` se promovió a `yahweh-widget-stat-card` y se importa
|
// `stat_card` se promovió a `nahual-widget-stat-card` y se importa
|
||||||
// arriba. La fn local fue eliminada en la iter 15 del refactor.
|
// arriba. La fn local fue eliminada en la iter 15 del refactor.
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-compositor"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada — el Cuerpo del compositor: un compositor Wayland teselante sobre smithay (backend winit, nested). Tesela con un Cerebro embebido o uno externo por mirada-link."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada-compositor"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
|
||||||
|
mirada-body = { path = "../../modules/mirada/mirada-body" }
|
||||||
|
mirada-link = { path = "../../modules/mirada/mirada-link" }
|
||||||
|
brahman-auth = { path = "../../protocol/brahman-auth" }
|
||||||
|
nix = { workspace = true }
|
||||||
|
smithay = "0.7"
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# mirada-compositor — el Cuerpo de carmen
|
||||||
|
|
||||||
|
Un compositor Wayland teselante real, sobre [`smithay`]. Es el **Cuerpo**
|
||||||
|
de la arquitectura Cerebro↔Cuerpo de `mirada` (ver
|
||||||
|
`crates/modules/mirada/SDD.md`): habla el protocolo Wayland con los
|
||||||
|
clientes, compone sus superficies y aplica la geometría que decide el
|
||||||
|
Cerebro.
|
||||||
|
|
||||||
|
Tiene **dos backends gráficos**:
|
||||||
|
|
||||||
|
- **`winit`** — corre **anidado**, como una ventana dentro de tu sesión
|
||||||
|
gráfica actual (X11 o Wayland). Para desarrollar y probar sin dejar el
|
||||||
|
escritorio.
|
||||||
|
- **`drm`** — corre **nativo** sobre una TTY, sin sesión anfitriona:
|
||||||
|
toma la GPU (DRM/KMS/GBM/EGL), el teclado (`libinput`) y la pantalla
|
||||||
|
entera. Es carmen como tu escritorio de verdad.
|
||||||
|
|
||||||
|
Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY` → `winit`;
|
||||||
|
sin ellos → `drm`. O fuérzalo: `mirada-compositor --winit` / `--drm`.
|
||||||
|
|
||||||
|
La bandera `--greeter` (ortogonal al backend) arranca el compositor como
|
||||||
|
gestor de login — ver **Modo greeter (DM)** más abajo.
|
||||||
|
|
||||||
|
## Backends
|
||||||
|
|
||||||
|
### winit — anidado
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p mirada-compositor -- --winit
|
||||||
|
```
|
||||||
|
|
||||||
|
Necesita una sesión gráfica anfitriona (X11 o Wayland) donde dibujar su
|
||||||
|
ventana; sin ella aborta con un mensaje que lo explica.
|
||||||
|
|
||||||
|
### drm — nativo sobre TTY
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p mirada-compositor -- --drm
|
||||||
|
```
|
||||||
|
|
||||||
|
Corre directo sobre el hardware. Requiere una **TTY** (`Ctrl+Alt+F3`),
|
||||||
|
una GPU con `/dev/dri`, y `seatd` o `logind` para la sesión. Toma la
|
||||||
|
pantalla completa; sal con `Super+Shift+e` o `Ctrl+C`.
|
||||||
|
|
||||||
|
Lleva teclado y ratón por `libinput`: el foco sigue al puntero y los
|
||||||
|
clics y la rueda llegan a la ventana que tienes debajo. El cursor toma
|
||||||
|
la forma que pide el cliente (la «I» sobre texto, una mano…) y cae a un
|
||||||
|
cuadrado por defecto sobre el escritorio. **`Super`+arrastre** con el
|
||||||
|
botón izquierdo mueve una ventana, con el derecho la redimensiona — al
|
||||||
|
arrastrarla, la ventana pasa a flotar. Cada ventana lleva un marco
|
||||||
|
fino: azul la que tiene el foco, gris las demás.
|
||||||
|
|
||||||
|
- `MIRADA_STARTUP=<cmd>` — lanza una app al arrancar (`MIRADA_STARTUP=foot`).
|
||||||
|
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
|
||||||
|
(0 o sin definir = sin tope).
|
||||||
|
|
||||||
|
## Como sesión de escritorio
|
||||||
|
|
||||||
|
Para usar carmen como tu escritorio de verdad — entrar a una sesión, no
|
||||||
|
sólo probarlo:
|
||||||
|
|
||||||
|
1. Compila e instala los binarios en el `PATH`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release -p mirada-compositor -p mirada-ctl -p mirada-launcher
|
||||||
|
sudo install -m755 target/release/mirada-compositor \
|
||||||
|
target/release/mirada-ctl target/release/mirada-launcher /usr/local/bin/
|
||||||
|
sudo install -m755 session/mirada-session /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Arranca desde una TTY:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mirada-session
|
||||||
|
```
|
||||||
|
|
||||||
|
O regístralo en un gestor de login copiando `session/carmen.desktop`
|
||||||
|
a `/usr/share/wayland-sessions/` — aparecerá carmen como sesión.
|
||||||
|
|
||||||
|
3. **Autoarranque** — los programas que quieras al iniciar van en
|
||||||
|
`~/.config/mirada/autostart`, uno por línea (`#` comenta). Tienes un
|
||||||
|
ejemplo en `session/autostart.example`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.config/mirada
|
||||||
|
cp crates/apps/mirada-compositor/session/autostart.example \
|
||||||
|
~/.config/mirada/autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin
|
||||||
|
romper carmen.
|
||||||
|
|
||||||
|
## Modo greeter (DM)
|
||||||
|
|
||||||
|
`mirada-compositor --greeter` arranca el compositor como **gestor de
|
||||||
|
login**: en vez de la sesión, compone el greeter (`mirada-greeter`),
|
||||||
|
que lanza como proceso hijo. El usuario teclea sus credenciales; cuando
|
||||||
|
el login es válido el greeter emite un `SessionTicket` por su stdout y
|
||||||
|
el compositor **muta a modo sesión sin reiniciar el servidor Wayland**
|
||||||
|
— el mismo proceso, la misma GPU, las mismas ventanas («mutación
|
||||||
|
atómica»). Desde ahí baja privilegios al usuario autenticado
|
||||||
|
(`setuid`/`setgid` + grupos) para todo lo que lanza.
|
||||||
|
|
||||||
|
La bandera es ortogonal al backend: `--greeter` solo (auto), o
|
||||||
|
`--greeter --drm` / `--greeter --winit`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# DM real, sobre una TTY — el compositor corre como root: PAM lo exige
|
||||||
|
sudo mirada-compositor --greeter --drm
|
||||||
|
|
||||||
|
# iterar el greeter anidado, con credenciales de prueba
|
||||||
|
MIRADA_GREETER_MOCK=demo:demo \
|
||||||
|
cargo run -p mirada-compositor -- --greeter --winit
|
||||||
|
```
|
||||||
|
|
||||||
|
En modo greeter no se registran atajos (todas las teclas van al
|
||||||
|
greeter — que el usuario no pueda lanzar nada ni cerrar el compositor),
|
||||||
|
se rechaza `spawn:` y no corre el autoarranque; los atajos y la sesión
|
||||||
|
arrancan sólo tras el traspaso. `MIRADA_GREETER_BIN` apunta a otro
|
||||||
|
binario de greeter (cómodo para señalar a `target/…` en desarrollo).
|
||||||
|
|
||||||
|
## Lanzador de aplicaciones
|
||||||
|
|
||||||
|
`mirada-launcher` escanea los `.desktop` del sistema y lanza el que
|
||||||
|
elijas. Es un programa de terminal sin dependencias: lo abres en una
|
||||||
|
terminal pequeña y filtras escribiendo. El keymap por defecto ata
|
||||||
|
`Super+p` a `spawn:foot -e mirada-launcher` — pulsa el atajo, escribe
|
||||||
|
unas letras del nombre, Enter.
|
||||||
|
|
||||||
|
Necesita `mirada-launcher` y `foot` en el `PATH` (ver la instalación de
|
||||||
|
arriba). Suelto también vale: `mirada-launcher` en cualquier terminal.
|
||||||
|
|
||||||
|
## Dos modos
|
||||||
|
|
||||||
|
- **Autónomo** (por defecto) — lleva un `Desktop` (de `mirada-brain`)
|
||||||
|
embebido. Es un compositor teselante completo en un solo proceso.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p mirada-compositor
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Enlazado** — el Cuerpo escucha en un socket y la app `mirada` (el
|
||||||
|
Cerebro GPUI) se conecta y decide la geometría.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# terminal 1 — el Cuerpo
|
||||||
|
MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada-compositor
|
||||||
|
# terminal 2 — el Cerebro
|
||||||
|
MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada
|
||||||
|
```
|
||||||
|
|
||||||
|
## Probarlo
|
||||||
|
|
||||||
|
Al arrancar imprime el `WAYLAND_DISPLAY` que abrió. Lanza cualquier
|
||||||
|
cliente Wayland contra él:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WAYLAND_DISPLAY=wayland-1 foot # o weston-terminal, alacritty, …
|
||||||
|
```
|
||||||
|
|
||||||
|
Las ventanas se teselan solas. El teclado, con la ventana del compositor
|
||||||
|
enfocada, maneja el escritorio con atajos `Super+…`: el lanzador de
|
||||||
|
aplicaciones `Super+p`, una terminal `Super+Shift+Return`, foco
|
||||||
|
`Super+j/k`, los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con
|
||||||
|
`Super+space`), área maestra `Super+h/l`, `nmaster` `Super+,/.`,
|
||||||
|
promover a maestra `Super+Return`, escritorios `Super+1..9`, cerrar
|
||||||
|
`Super+q`. Cierra la ventana del compositor para salir.
|
||||||
|
|
||||||
|
## Atajos de teclado
|
||||||
|
|
||||||
|
Los atajos son configurables en RON: `~/.config/mirada/keymap.ron`. En
|
||||||
|
modo autónomo, el Cuerpo lo carga al arrancar (si no existe, escribe uno
|
||||||
|
por defecto documentado) y lo **recarga en caliente** — edita el archivo,
|
||||||
|
guarda, y los atajos cambian sin reiniciar. En modo enlazado el keymap es
|
||||||
|
asunto del Cerebro (la app `mirada`).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p mirada-brain --example keymap-default # ver el formato
|
||||||
|
```
|
||||||
|
|
||||||
|
El compositor en sí no interpreta atajos: sólo intercepta las
|
||||||
|
combinaciones que el Cerebro le pide (`GrabKeys`) y le devuelve la
|
||||||
|
pulsada. *Qué significa* cada una lo decide `mirada-brain`. Ver el SDD.
|
||||||
|
|
||||||
|
## Control externo
|
||||||
|
|
||||||
|
En modo autónomo, el compositor abre un socket de control y `mirada-ctl`
|
||||||
|
lo maneja desde la terminal — al estilo de `swaymsg`/`hyprctl`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mirada-ctl focus-next # cambia el foco
|
||||||
|
mirada-ctl focus-window 5 # enfoca una ventana concreta
|
||||||
|
mirada-ctl workspace 3 # va al escritorio 3
|
||||||
|
mirada-ctl windows # lista las ventanas
|
||||||
|
```
|
||||||
|
|
||||||
|
En modo enlazado el socket de control lo abre el Cerebro (la app
|
||||||
|
`mirada`), no el compositor.
|
||||||
|
|
||||||
|
## Qué implementa
|
||||||
|
|
||||||
|
`wl_compositor`, `xdg_shell` (toplevels y popups), `wl_shm`, `wl_seat`
|
||||||
|
(teclado, y ratón en el backend DRM), `wl_output`, `wl_data_device`
|
||||||
|
(selección), `xdg-decoration` — fuerza decoración del servidor y no
|
||||||
|
dibuja ninguna, así las ventanas van sin barra de título — y
|
||||||
|
`zwp_linux_dmabuf`, que deja conectarse a los clientes que pintan por
|
||||||
|
GPU (apps GPUI, navegadores acelerados). Composición con `GlesRenderer`
|
||||||
|
— en `winit` sobre la ventana, en `drm` con un `DrmCompositor` por
|
||||||
|
salida.
|
||||||
|
|
||||||
|
Reusa `mirada-body` para la contabilidad de salidas y superficies, y
|
||||||
|
`mirada-link` para el cable hacia un Cerebro externo. Toda la lógica
|
||||||
|
espacial es agnóstica de Wayland y vive en los crates de
|
||||||
|
`crates/modules/mirada/`.
|
||||||
|
|
||||||
|
## Pendiente
|
||||||
|
|
||||||
|
Del backend DRM: conmutación de VT, hotplug de monitores, multi-GPU.
|
||||||
|
Puntero en el backend `winit`. Aislamiento de clientes. Ver el SDD.
|
||||||
|
|
||||||
|
[`smithay`]: https://github.com/Smithay/smithay
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Autoarranque de carmen — cópialo a ~/.config/mirada/autostart
|
||||||
|
#
|
||||||
|
# Un comando por línea; se lanza al arrancar el compositor, con
|
||||||
|
# WAYLAND_DISPLAY ya puesto. Las líneas en blanco y las que empiezan
|
||||||
|
# por # se ignoran. Cada línea se pasa a `sh -c`, así que valen las
|
||||||
|
# variables, las tuberías y el `&` final no hace falta.
|
||||||
|
|
||||||
|
# El shell de carmen — barra acoplada al pie con su línea de comandos.
|
||||||
|
# carmen la reconoce por su app_id y le reserva la franja.
|
||||||
|
shuma-shell --launcher
|
||||||
|
|
||||||
|
# Una terminal para empezar.
|
||||||
|
foot
|
||||||
|
|
||||||
|
# Ejemplos (descoméntalos si los quieres):
|
||||||
|
# mirada-ctl layout spiral
|
||||||
|
# wbg ~/fondo.png
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=carmen
|
||||||
|
Comment=Compositor Wayland teselante (mirada)
|
||||||
|
Exec=mirada-session
|
||||||
|
Type=Application
|
||||||
|
DesktopNames=carmen
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# mirada-session — arranca carmen (el compositor mirada) como una sesión
|
||||||
|
# de escritorio. Pensado para lanzarse desde una TTY o desde un gestor de
|
||||||
|
# login (greetd, ly, …).
|
||||||
|
#
|
||||||
|
# Instálalo en el PATH (p. ej. /usr/local/bin/mirada-session) junto al
|
||||||
|
# binario `mirada-compositor`.
|
||||||
|
|
||||||
|
# Carmen es un compositor Wayland.
|
||||||
|
export XDG_SESSION_TYPE=wayland
|
||||||
|
export XDG_CURRENT_DESKTOP=carmen
|
||||||
|
export XDG_SESSION_DESKTOP=carmen
|
||||||
|
|
||||||
|
# Que las apps GUI prefieran sus backends Wayland.
|
||||||
|
export MOZ_ENABLE_WAYLAND=1
|
||||||
|
export QT_QPA_PLATFORM="wayland;xcb"
|
||||||
|
export SDL_VIDEODRIVER=wayland
|
||||||
|
export _JAVA_AWT_WM_NONREPARENTING=1
|
||||||
|
|
||||||
|
# El backend DRM toma la TTY entera. Los programas de arranque van en
|
||||||
|
# ~/.config/mirada/autostart (uno por línea).
|
||||||
|
exec mirada-compositor --drm
|
||||||
@@ -0,0 +1,937 @@
|
|||||||
|
//! `drm_backend` — el Cuerpo del compositor sobre **DRM/KMS**, sin
|
||||||
|
//! sesión gráfica anfitriona: corre directo sobre una TTY, como tu
|
||||||
|
//! escritorio de verdad.
|
||||||
|
//!
|
||||||
|
//! Construido por fases para verificarlo en hardware paso a paso:
|
||||||
|
//!
|
||||||
|
//! - **Fase 1 — bring-up**: sesión (`libseat`), GPU, dispositivo DRM,
|
||||||
|
//! enumerar salidas.
|
||||||
|
//! - **Fase 2a — pipeline de render**: GBM, EGL y `GlesRenderer`, con un
|
||||||
|
//! `DrmCompositor` para la salida conectada.
|
||||||
|
//! - **Fase 2b — bucle Wayland** (esto): un bucle `calloop` que atiende
|
||||||
|
//! a los clientes Wayland, el teclado (`libinput`) y el VBlank, y
|
||||||
|
//! compone las ventanas de verdad. Aquí `mirada-compositor --drm` ya
|
||||||
|
//! es un escritorio funcionando.
|
||||||
|
//!
|
||||||
|
//! Todo con logs para diagnosticar sin el hardware delante.
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags};
|
||||||
|
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||||
|
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||||
|
use smithay::backend::input::{
|
||||||
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent,
|
||||||
|
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||||
|
};
|
||||||
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||||
|
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||||
|
use smithay::backend::renderer::element::surface::{
|
||||||
|
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::{render_elements, Id, Kind};
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::backend::renderer::{ImportAll, ImportDma};
|
||||||
|
use smithay::backend::session::libseat::LibSeatSession;
|
||||||
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||||
|
use smithay::backend::udev;
|
||||||
|
use smithay::input::keyboard::FilterResult;
|
||||||
|
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
|
||||||
|
use smithay::output::OutputModeSource;
|
||||||
|
use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
|
||||||
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
|
use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
|
||||||
|
use smithay::reexports::drm::control::connector::State as ConnectorState;
|
||||||
|
use smithay::reexports::drm::control::{Device as ControlDevice, ModeTypeFlags};
|
||||||
|
use smithay::reexports::input::Libinput;
|
||||||
|
use smithay::reexports::rustix::fs::OFlags;
|
||||||
|
use smithay::reexports::wayland_server::{Display, ListeningSocket};
|
||||||
|
use smithay::utils::{
|
||||||
|
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
use brahman_auth::SessionTicket;
|
||||||
|
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
|
||||||
|
Setup,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// El `DrmCompositor` concreto para la salida (un solo GPU).
|
||||||
|
type Compositor =
|
||||||
|
DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
|
||||||
|
|
||||||
|
render_elements! {
|
||||||
|
/// Lo que el backend DRM compone en un cuadro: superficies de cliente
|
||||||
|
/// y rectángulos de color sólido (el cursor y los marcos de ventana).
|
||||||
|
Frame<R> where R: ImportAll;
|
||||||
|
Window = WaylandSurfaceRenderElement<R>,
|
||||||
|
Solid = SolidColorRenderElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color de fondo del escritorio cuando no hay nada que lo tape.
|
||||||
|
const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0];
|
||||||
|
|
||||||
|
/// Lado del cursor de software, en píxeles.
|
||||||
|
const CURSOR_SIZE: i32 = 12;
|
||||||
|
|
||||||
|
/// Color del cursor — un cuadrado casi blanco, opaco.
|
||||||
|
const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0];
|
||||||
|
|
||||||
|
/// Lado mínimo de una ventana al redimensionarla con el ratón.
|
||||||
|
const MIN_WINDOW: i32 = 120;
|
||||||
|
|
||||||
|
/// Grosor del marco de una ventana, en píxeles.
|
||||||
|
const BORDER_WIDTH: i32 = 2;
|
||||||
|
|
||||||
|
/// Color del marco de la ventana enfocada — un azul que resalta.
|
||||||
|
const BORDER_FOCUS: [f32; 4] = [0.36, 0.56, 0.92, 1.0];
|
||||||
|
|
||||||
|
/// Color del marco de las ventanas sin foco — gris discreto.
|
||||||
|
const BORDER_NORMAL: [f32; 4] = [0.22, 0.22, 0.27, 1.0];
|
||||||
|
|
||||||
|
/// Los 4 rectángulos `(x, y, w, h)` del marco de una ventana cuyo
|
||||||
|
/// contenido ocupa `(sx, sy, sw, sh)`. El marco va *hacia adentro* (pisa
|
||||||
|
/// el borde de la superficie), así nunca se solapa con el de la ventana
|
||||||
|
/// vecina: arriba, abajo, izquierda, derecha.
|
||||||
|
fn border_rects(sx: i32, sy: i32, sw: i32, sh: i32) -> [(i32, i32, i32, i32); 4] {
|
||||||
|
let bw = BORDER_WIDTH;
|
||||||
|
let side_h = (sh - 2 * bw).max(0);
|
||||||
|
[
|
||||||
|
(sx, sy, sw, bw),
|
||||||
|
(sx, sy + sh - bw, sw, bw),
|
||||||
|
(sx, sy + bw, bw, side_h),
|
||||||
|
(sx + sw - bw, sy + bw, bw, side_h),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Códigos de botón de `<linux/input-event-codes.h>`.
|
||||||
|
const BTN_LEFT: u32 = 0x110;
|
||||||
|
const BTN_RIGHT: u32 = 0x111;
|
||||||
|
|
||||||
|
/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
|
||||||
|
struct DrmState {
|
||||||
|
app: App,
|
||||||
|
display: Display<App>,
|
||||||
|
/// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
|
||||||
|
/// conmutar de VT.
|
||||||
|
drm: DrmDevice,
|
||||||
|
compositor: Compositor,
|
||||||
|
renderer: GlesRenderer,
|
||||||
|
/// Contexto `libinput` — se suspende y reanuda al conmutar de VT.
|
||||||
|
libinput: Libinput,
|
||||||
|
/// `false` mientras la sesión está cedida a otra VT — no se compone.
|
||||||
|
active: bool,
|
||||||
|
/// `true` entre que se encola un page-flip y llega su VBlank.
|
||||||
|
pending_flip: bool,
|
||||||
|
keymap_path: Option<std::path::PathBuf>,
|
||||||
|
keymap_watch: Option<mirada_brain::KeymapWatch>,
|
||||||
|
ctl: Option<crate::CtlServer>,
|
||||||
|
/// Inicio del compositor — base de tiempos para los frame-callbacks.
|
||||||
|
start: Instant,
|
||||||
|
/// Nº de ventanas en el último `tick` — para registrar los cambios.
|
||||||
|
last_windows: usize,
|
||||||
|
/// Identidad estable del cursor de software — el seguimiento de daño
|
||||||
|
/// la usa para no recomponer todo cuando el cursor sólo se mueve.
|
||||||
|
cursor_id: Id,
|
||||||
|
/// Ventana sobre la que estaba el puntero — para el foco-sigue-ratón.
|
||||||
|
last_pointer_window: Option<u64>,
|
||||||
|
/// Tamaño de la salida, en píxeles — los topes del puntero.
|
||||||
|
output_size: (f64, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrmState {
|
||||||
|
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
|
||||||
|
fn render(&mut self) {
|
||||||
|
if !self.active {
|
||||||
|
return; // la sesión está en otra VT — no tocamos la GPU
|
||||||
|
}
|
||||||
|
if self.pending_flip {
|
||||||
|
return; // aún esperamos el VBlank del cuadro anterior
|
||||||
|
}
|
||||||
|
let output_h = self.app.output_size.1;
|
||||||
|
|
||||||
|
// Paso 1 · refresca los búferes del marco de cada ventana — su
|
||||||
|
// tamaño (sigue al contenido) y su color (según el foco). Cada
|
||||||
|
// `SolidColorBuffer` sube su contador de daño sólo si algo cambió.
|
||||||
|
for w in &mut self.app.windows {
|
||||||
|
if !w.visible || w.is_shell {
|
||||||
|
continue; // el shell no lleva marco
|
||||||
|
}
|
||||||
|
let (x, y) = crate::render_loc(w, output_h);
|
||||||
|
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
|
||||||
|
let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
|
||||||
|
let rects = border_rects(x, y, sw, sh);
|
||||||
|
for (buf, (_, _, bw, bh)) in w.borders.iter_mut().zip(rects) {
|
||||||
|
buf.update((bw, bh), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paso 2 · arma los elementos — lista front-to-back (índice 0 =
|
||||||
|
// encima): el cursor, y por cada ventana su marco sobre su
|
||||||
|
// superficie. Las flotantes van antes que las teseladas.
|
||||||
|
let elements: Vec<Frame<GlesRenderer>> = {
|
||||||
|
let mut out: Vec<Frame<GlesRenderer>> = Vec::new();
|
||||||
|
|
||||||
|
// El cursor — la superficie que pidió el cliente (la «I» del
|
||||||
|
// texto, una mano…), o el cuadrado por defecto si pidió un
|
||||||
|
// cursor con nombre y no hay tema. `Hidden` no pinta nada.
|
||||||
|
let (cx, cy) = self.app.pointer_loc;
|
||||||
|
match &self.app.cursor_status {
|
||||||
|
CursorImageStatus::Hidden => {}
|
||||||
|
CursorImageStatus::Surface(surface) if surface.alive() => {
|
||||||
|
let (hx, hy) = crate::cursor_hotspot(surface);
|
||||||
|
let loc = (cx.round() as i32 - hx, cy.round() as i32 - hy);
|
||||||
|
for el in render_elements_from_surface_tree(
|
||||||
|
&mut self.renderer,
|
||||||
|
surface,
|
||||||
|
loc,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
Kind::Cursor,
|
||||||
|
) {
|
||||||
|
out.push(Frame::Window(el));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let cursor_rect = Rectangle::new(
|
||||||
|
Point::<i32, Physical>::from((cx.round() as i32, cy.round() as i32)),
|
||||||
|
Size::<i32, Physical>::from((CURSOR_SIZE, CURSOR_SIZE)),
|
||||||
|
);
|
||||||
|
out.push(Frame::Solid(SolidColorRenderElement::new(
|
||||||
|
self.cursor_id.clone(),
|
||||||
|
cursor_rect,
|
||||||
|
CommitCounter::default(),
|
||||||
|
CURSOR_COLOR,
|
||||||
|
Kind::Cursor,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// El shell va sobre todo; luego las flotantes; luego las
|
||||||
|
// teseladas. `sort_by_key` es estable: respeta el orden de
|
||||||
|
// apertura dentro de cada grupo.
|
||||||
|
let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
|
||||||
|
shown.sort_by_key(|w| (!w.is_shell, !w.floating));
|
||||||
|
for w in &shown {
|
||||||
|
let (x, y) = crate::render_loc(w, output_h);
|
||||||
|
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
|
||||||
|
// El marco, encima de la propia superficie de la ventana
|
||||||
|
// — el shell no lleva.
|
||||||
|
if !w.is_shell {
|
||||||
|
let rects = border_rects(x, y, sw, sh);
|
||||||
|
for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
|
||||||
|
out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
|
||||||
|
buf,
|
||||||
|
(bx, by),
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for el in render_elements_from_surface_tree(
|
||||||
|
&mut self.renderer,
|
||||||
|
&w.surface,
|
||||||
|
(x, y),
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
Kind::Unspecified,
|
||||||
|
) {
|
||||||
|
out.push(Frame::Window(el));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
match self.compositor.render_frame::<_, _>(
|
||||||
|
&mut self.renderer,
|
||||||
|
&elements,
|
||||||
|
CLEAR_COLOR,
|
||||||
|
FrameFlags::DEFAULT,
|
||||||
|
) {
|
||||||
|
Ok(result) => {
|
||||||
|
if !result.is_empty {
|
||||||
|
match self.compositor.queue_frame(()) {
|
||||||
|
Ok(()) => self.pending_flip = true,
|
||||||
|
Err(e) => eprintln!("mirada-compositor · queue_frame: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("mirada-compositor · render_frame: {e}"),
|
||||||
|
}
|
||||||
|
// Avisa a cada cliente de que puede dibujar el siguiente cuadro.
|
||||||
|
let time = self.start.elapsed().as_millis() as u32;
|
||||||
|
for w in &self.app.windows {
|
||||||
|
send_frames_surface_tree(&w.surface, time);
|
||||||
|
}
|
||||||
|
// También a la superficie del cursor, por si es un cursor animado.
|
||||||
|
if let CursorImageStatus::Surface(surface) = &self.app.cursor_status {
|
||||||
|
if surface.alive() {
|
||||||
|
send_frames_surface_tree(surface, time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La sesión se cede a otra VT (`Ctrl+Alt+Fn`): suelta la GPU y deja
|
||||||
|
/// de leer el ratón y el teclado, para no chocar con quien ahora
|
||||||
|
/// manda en la pantalla.
|
||||||
|
fn pause_session(&mut self) {
|
||||||
|
self.active = false;
|
||||||
|
self.drm.pause();
|
||||||
|
self.libinput.suspend();
|
||||||
|
println!("mirada-compositor · sesión cedida a otra VT.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La sesión vuelve a esta VT: recupera la GPU y la entrada, reinicia
|
||||||
|
/// el estado del compositor y repinta.
|
||||||
|
fn resume_session(&mut self) {
|
||||||
|
if self.libinput.resume().is_err() {
|
||||||
|
eprintln!("mirada-compositor · libinput.resume falló.");
|
||||||
|
}
|
||||||
|
if let Err(e) = self.drm.activate(false) {
|
||||||
|
eprintln!("mirada-compositor · drm.activate falló: {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = self.compositor.reset_state() {
|
||||||
|
eprintln!("mirada-compositor · compositor.reset_state falló: {e}");
|
||||||
|
}
|
||||||
|
self.active = true;
|
||||||
|
self.pending_flip = false;
|
||||||
|
self.render();
|
||||||
|
println!("mirada-compositor · sesión recuperada.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
|
||||||
|
/// control, composición y vaciado hacia los clientes.
|
||||||
|
fn tick(&mut self) {
|
||||||
|
self.app.brain_poll();
|
||||||
|
|
||||||
|
let n = self.app.windows.len();
|
||||||
|
if n != self.last_windows {
|
||||||
|
eprintln!("mirada-compositor · ventanas en pantalla: {n}");
|
||||||
|
self.last_windows = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.keymap_watch.as_ref().is_some_and(|w| w.changed()) {
|
||||||
|
if let Some(path) = &self.keymap_path {
|
||||||
|
match Keymap::load(path) {
|
||||||
|
Ok(km) => {
|
||||||
|
let cmd = if let Brain::Embedded(d) = &mut self.app.brain {
|
||||||
|
Some(d.set_keymap(km))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(cmd) = cmd {
|
||||||
|
self.app.apply_commands(vec![cmd]);
|
||||||
|
}
|
||||||
|
println!("mirada-compositor · keymap recargado.");
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("mirada-compositor · keymap inválido: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ctl) = &self.ctl {
|
||||||
|
while let Some(mut conn) = ctl.poll() {
|
||||||
|
let reply = match conn.read_request() {
|
||||||
|
Ok(Some(req)) => self.app.serve_ctl(req),
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(e) => CtlReply::Error(format!("{e}")),
|
||||||
|
};
|
||||||
|
let _ = conn.reply(&reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render();
|
||||||
|
let _ = self.display.flush_clients();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procesa un evento de `libinput`: teclado y puntero.
|
||||||
|
fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
|
||||||
|
let time = self.start.elapsed().as_millis() as u32;
|
||||||
|
match event {
|
||||||
|
// --- Teclado: intercepta los atajos del Cerebro --------------
|
||||||
|
InputEvent::Keyboard { event } => {
|
||||||
|
let Some(keyboard) = self.app.keyboard.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let code = event.key_code();
|
||||||
|
let key_state = event.state();
|
||||||
|
let pressed = key_state == KeyState::Pressed;
|
||||||
|
keyboard.input::<(), _>(
|
||||||
|
&mut self.app,
|
||||||
|
code,
|
||||||
|
key_state,
|
||||||
|
SERIAL_COUNTER.next_serial(),
|
||||||
|
time,
|
||||||
|
|st, mods, handle| {
|
||||||
|
if !pressed {
|
||||||
|
return FilterResult::Forward;
|
||||||
|
}
|
||||||
|
if let Some(combo) = combo_string(mods, handle.modified_sym()) {
|
||||||
|
if st.grabs.contains(&combo) {
|
||||||
|
st.pending_keybind = Some(combo);
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilterResult::Forward
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Some(combo) = self.app.pending_keybind.take() {
|
||||||
|
let ev = self.app.body.keybind(combo);
|
||||||
|
self.app.brain_feed(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Puntero: movimiento relativo (ratón, touchpad) ----------
|
||||||
|
InputEvent::PointerMotion { event } => {
|
||||||
|
let (mut x, mut y) = self.app.pointer_loc;
|
||||||
|
x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
|
||||||
|
y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
|
||||||
|
self.app.pointer_loc = (x, y);
|
||||||
|
if !self.drag_update() {
|
||||||
|
self.pointer_motion(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Puntero: movimiento absoluto (táctil, tableta) ----------
|
||||||
|
InputEvent::PointerMotionAbsolute { event } => {
|
||||||
|
let space = Size::<i32, Logical>::from((
|
||||||
|
self.output_size.0 as i32,
|
||||||
|
self.output_size.1 as i32,
|
||||||
|
));
|
||||||
|
let pos = event.position_transformed(space);
|
||||||
|
self.app.pointer_loc = (
|
||||||
|
pos.x.clamp(0.0, self.output_size.0),
|
||||||
|
pos.y.clamp(0.0, self.output_size.1),
|
||||||
|
);
|
||||||
|
if !self.drag_update() {
|
||||||
|
self.pointer_motion(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Puntero: botones ----------------------------------------
|
||||||
|
InputEvent::PointerButton { event } => {
|
||||||
|
let pressed = event.state() == ButtonState::Pressed;
|
||||||
|
let button = event.button_code();
|
||||||
|
|
||||||
|
// ¿Empieza un arrastre? `Super`+botón sobre una ventana:
|
||||||
|
// izquierdo mueve, derecho redimensiona.
|
||||||
|
if pressed && self.app.drag.is_none() {
|
||||||
|
let super_held = self
|
||||||
|
.app
|
||||||
|
.keyboard
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|kb| kb.modifier_state().logo);
|
||||||
|
let mode = match button {
|
||||||
|
BTN_LEFT if super_held => Some(DragMode::Move),
|
||||||
|
BTN_RIGHT if super_held => Some(DragMode::Resize),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(mode) = mode {
|
||||||
|
let (x, y) = self.app.pointer_loc;
|
||||||
|
if let Some(i) = self.window_at(x, y) {
|
||||||
|
let w = &self.app.windows[i];
|
||||||
|
let grab = DragGrab {
|
||||||
|
id: w.id,
|
||||||
|
mode,
|
||||||
|
start_pointer: (x, y),
|
||||||
|
start_rect: (w.loc.0, w.loc.1, w.size.0, w.size.1),
|
||||||
|
};
|
||||||
|
self.app.drag = Some(grab);
|
||||||
|
return; // el arrastre captura el botón
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durante un arrastre los botones no llegan al cliente;
|
||||||
|
// soltar cualquiera lo termina.
|
||||||
|
if self.app.drag.is_some() {
|
||||||
|
if !pressed {
|
||||||
|
self.app.drag = None;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botón normal: a la ventana bajo el puntero.
|
||||||
|
let Some(pointer) = self.app.pointer.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
pointer.button(
|
||||||
|
&mut self.app,
|
||||||
|
&ButtonEvent {
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time,
|
||||||
|
button,
|
||||||
|
state: event.state(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(&mut self.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Puntero: rueda / desplazamiento -------------------------
|
||||||
|
InputEvent::PointerAxis { event } => {
|
||||||
|
let Some(pointer) = self.app.pointer.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let source = event.source();
|
||||||
|
let mut frame = AxisFrame::new(time).source(source);
|
||||||
|
for axis in [Axis::Horizontal, Axis::Vertical] {
|
||||||
|
match event.amount(axis) {
|
||||||
|
Some(v) if v != 0.0 => frame = frame.value(axis, v),
|
||||||
|
Some(_) if source == AxisSource::Finger => {
|
||||||
|
frame = frame.stop(axis);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(d) = event.amount_v120(axis) {
|
||||||
|
frame = frame.v120(axis, d as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pointer.axis(&mut self.app, frame);
|
||||||
|
pointer.frame(&mut self.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {} // otros dispositivos: aún no
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reenvía el puntero a la ventana que tiene debajo y, si esa ventana
|
||||||
|
/// cambió, aplica el foco-sigue-ratón avisando al Cerebro.
|
||||||
|
fn pointer_motion(&mut self, time: u32) {
|
||||||
|
let Some(pointer) = self.app.pointer.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (x, y) = self.app.pointer_loc;
|
||||||
|
let hit = self.window_at(x, y);
|
||||||
|
let focus = hit.map(|i| {
|
||||||
|
let w = &self.app.windows[i];
|
||||||
|
let (lx, ly) = crate::render_loc(w, self.app.output_size.1);
|
||||||
|
(
|
||||||
|
w.surface.clone(),
|
||||||
|
Point::<f64, Logical>::from((lx as f64, ly as f64)),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
pointer.motion(
|
||||||
|
&mut self.app,
|
||||||
|
focus,
|
||||||
|
&MotionEvent {
|
||||||
|
location: Point::from((x, y)),
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(&mut self.app);
|
||||||
|
|
||||||
|
// Sobre el escritorio pelado no manda ningún cliente: el cursor
|
||||||
|
// vuelve al de por defecto (si no, se queda con la «I» del texto
|
||||||
|
// de la última ventana).
|
||||||
|
if hit.is_none() {
|
||||||
|
self.app.cursor_status = CursorImageStatus::default_named();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foco-sigue-ratón: al pasar a otra ventana, que la enfoque quien
|
||||||
|
// corresponda — el Cerebro para las teseladas, carmen mismo para
|
||||||
|
// el shell (que no vive en el Cerebro).
|
||||||
|
let hovered = hit.map(|i| self.app.windows[i].id);
|
||||||
|
if hovered != self.last_pointer_window {
|
||||||
|
self.last_pointer_window = hovered;
|
||||||
|
match hit {
|
||||||
|
Some(i) if self.app.windows[i].is_shell => {
|
||||||
|
let surf = self.app.windows[i].surface.clone();
|
||||||
|
if let Some(kb) = self.app.keyboard.clone() {
|
||||||
|
kb.set_focus(&mut self.app, Some(surf), SERIAL_COUNTER.next_serial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(i) => {
|
||||||
|
let id = self.app.windows[i].id;
|
||||||
|
let ev = self.app.body.pointer_enter(id);
|
||||||
|
self.app.brain_feed(ev);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Si hay un arrastre en curso, recalcula el rectángulo de la ventana
|
||||||
|
/// y se lo manda al Cerebro (que la hace flotar ahí). Devuelve `true`
|
||||||
|
/// si consumió el movimiento — entonces el puntero no llega al cliente.
|
||||||
|
fn drag_update(&mut self) -> bool {
|
||||||
|
let Some(drag) = self.app.drag.as_ref() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mode = drag.mode;
|
||||||
|
let (spx, spy) = drag.start_pointer;
|
||||||
|
let (sx, sy, sw, sh) = drag.start_rect;
|
||||||
|
let id = drag.id;
|
||||||
|
|
||||||
|
let (px, py) = self.app.pointer_loc;
|
||||||
|
let dx = (px - spx) as i32;
|
||||||
|
let dy = (py - spy) as i32;
|
||||||
|
let rect = match mode {
|
||||||
|
DragMode::Move => Rect::new(sx + dx, sy + dy, sw, sh),
|
||||||
|
DragMode::Resize => Rect::new(
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
(sw + dx).max(MIN_WINDOW),
|
||||||
|
(sh + dy).max(MIN_WINDOW),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.app.brain_feed(BodyEvent::WindowFloatTo { id, rect });
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El índice de la ventana visible bajo el punto `(x, y)`, si la hay
|
||||||
|
/// — en orden front-to-back (el shell gana a las flotantes, y éstas a
|
||||||
|
/// las teseladas).
|
||||||
|
fn window_at(&self, x: f64, y: f64) -> Option<usize> {
|
||||||
|
let mut idx: Vec<usize> = (0..self.app.windows.len())
|
||||||
|
.filter(|&i| self.app.windows[i].visible)
|
||||||
|
.collect();
|
||||||
|
idx.sort_by_key(|&i| {
|
||||||
|
let w = &self.app.windows[i];
|
||||||
|
(!w.is_shell, !w.floating)
|
||||||
|
});
|
||||||
|
let output_h = self.app.output_size.1;
|
||||||
|
idx.into_iter().find(|&i| {
|
||||||
|
let w = &self.app.windows[i];
|
||||||
|
let (lx, ly) = crate::render_loc(w, output_h);
|
||||||
|
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
|
||||||
|
x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
|
||||||
|
/// el compositor nace en modo DM: ver [`BodyMode`].
|
||||||
|
pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
|
||||||
|
println!("mirada-compositor · backend DRM.");
|
||||||
|
println!("──────────────────────────────────────────────────");
|
||||||
|
|
||||||
|
// 1 · Sesión.
|
||||||
|
println!("[1/8] abriendo la sesión (libseat) …");
|
||||||
|
let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"no pude abrir la sesión libseat: {e}\n \
|
||||||
|
¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let seat_name = session.seat();
|
||||||
|
println!(" sesión abierta · seat «{seat_name}»");
|
||||||
|
|
||||||
|
// 2 · GPU primaria.
|
||||||
|
println!("[2/8] buscando la GPU primaria …");
|
||||||
|
let gpu = udev::primary_gpu(&seat_name)
|
||||||
|
.map_err(|e| format!("error consultando udev: {e}"))?
|
||||||
|
.ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?;
|
||||||
|
println!(" GPU primaria: {}", gpu.display());
|
||||||
|
|
||||||
|
// 3 · Dispositivo DRM.
|
||||||
|
println!("[3/8] abriendo el dispositivo DRM …");
|
||||||
|
let fd = session
|
||||||
|
.open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK)
|
||||||
|
.map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?;
|
||||||
|
let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
||||||
|
let (mut drm, drm_notifier) =
|
||||||
|
DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
|
||||||
|
println!(" dispositivo DRM listo.");
|
||||||
|
|
||||||
|
// 4 · Elegir la salida conectada: conector + CRTC + modo.
|
||||||
|
println!("[4/8] eligiendo salida …");
|
||||||
|
let resources = drm
|
||||||
|
.resource_handles()
|
||||||
|
.map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
|
||||||
|
let mut chosen = None;
|
||||||
|
for &conn_handle in resources.connectors() {
|
||||||
|
let conn = match drm.get_connector(conn_handle, false) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if conn.state() != ConnectorState::Connected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
|
||||||
|
// Registra todos los modos del panel — diagnóstico.
|
||||||
|
for m in conn.modes() {
|
||||||
|
let (mw, mh) = m.size();
|
||||||
|
let pref = if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
|
||||||
|
" [PREFERRED]"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
eprintln!(" modo de «{name}»: {mw}×{mh} @ {} Hz{pref}", m.vrefresh());
|
||||||
|
}
|
||||||
|
// Elige el modo de mayor área (a igualdad, mayor refresco) — el
|
||||||
|
// nativo del panel. La marca PREFERRED no es fiable: a veces
|
||||||
|
// señala un modo menor.
|
||||||
|
let mode = conn
|
||||||
|
.modes()
|
||||||
|
.iter()
|
||||||
|
.max_by_key(|m| {
|
||||||
|
let (mw, mh) = m.size();
|
||||||
|
(mw as u32 * mh as u32, m.vrefresh())
|
||||||
|
})
|
||||||
|
.copied();
|
||||||
|
let Some(mode) = mode else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let crtc = conn
|
||||||
|
.encoders()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|enc| drm.get_encoder(*enc).ok())
|
||||||
|
.find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next());
|
||||||
|
if let Some(crtc) = crtc {
|
||||||
|
let (w, h) = mode.size();
|
||||||
|
println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}");
|
||||||
|
chosen = Some((conn_handle, crtc, mode, name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (conn_handle, crtc, mode, out_name) =
|
||||||
|
chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
|
||||||
|
let (mode_w, mode_h) = mode.size();
|
||||||
|
|
||||||
|
// 5 · GBM + EGL + GlesRenderer.
|
||||||
|
println!("[5/8] inicializando GBM + EGL + GlesRenderer …");
|
||||||
|
let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?;
|
||||||
|
let egl_display =
|
||||||
|
unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?;
|
||||||
|
let egl_context =
|
||||||
|
EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
|
||||||
|
let renderer =
|
||||||
|
unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
|
||||||
|
println!(" renderer GLES listo.");
|
||||||
|
|
||||||
|
// 6 · Superficie DRM + DrmCompositor de la salida.
|
||||||
|
println!("[6/8] creando la superficie DRM y el compositor …");
|
||||||
|
let surface = drm
|
||||||
|
.create_surface(crtc, mode, &[conn_handle])
|
||||||
|
.map_err(|e| format!("create_surface falló: {e}"))?;
|
||||||
|
let allocator =
|
||||||
|
GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||||
|
let exporter = GbmFramebufferExporter::new(gbm.clone(), None);
|
||||||
|
let renderer_formats = renderer.dmabuf_formats();
|
||||||
|
let mode_source = OutputModeSource::Static {
|
||||||
|
size: Size::from((mode_w as i32, mode_h as i32)),
|
||||||
|
scale: Scale::from(1.0),
|
||||||
|
transform: Transform::Normal,
|
||||||
|
};
|
||||||
|
let compositor: Compositor = DrmCompositor::new(
|
||||||
|
mode_source,
|
||||||
|
surface,
|
||||||
|
None,
|
||||||
|
allocator,
|
||||||
|
exporter,
|
||||||
|
[Fourcc::Argb8888, Fourcc::Xrgb8888],
|
||||||
|
renderer_formats,
|
||||||
|
drm.cursor_size(),
|
||||||
|
Some(gbm.clone()),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
|
||||||
|
println!(" compositor de «{out_name}» listo.");
|
||||||
|
|
||||||
|
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
|
||||||
|
println!("[7/8] armando el estado Wayland …");
|
||||||
|
let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } =
|
||||||
|
crate::build_app(greeter)?;
|
||||||
|
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
|
||||||
|
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
|
||||||
|
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
|
||||||
|
// La salida del Cerebro = el modo del monitor.
|
||||||
|
let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
|
||||||
|
app.brain_feed(ev);
|
||||||
|
app.output_size = (mode_w as i32, mode_h as i32);
|
||||||
|
// El puntero arranca en el centro de la pantalla.
|
||||||
|
app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
|
||||||
|
// Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
|
||||||
|
let _wl_output = crate::announce_output(
|
||||||
|
&display.handle(),
|
||||||
|
&out_name,
|
||||||
|
mode_w as i32,
|
||||||
|
mode_h as i32,
|
||||||
|
mode.vrefresh() as i32 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// El socket Wayland por el que se conectan los clientes.
|
||||||
|
let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
|
||||||
|
let socket_name = listener
|
||||||
|
.socket_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("wayland-?")
|
||||||
|
.to_string();
|
||||||
|
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
||||||
|
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
|
||||||
|
|
||||||
|
// Modo DM: lanza el greeter y recibe su tiquet por un canal de
|
||||||
|
// `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
|
||||||
|
let greeter_rx = if app.mode == BodyMode::Greeter {
|
||||||
|
let (tx, rx) = ticket_channel::<SessionTicket>();
|
||||||
|
crate::spawn_greeter(move |ticket| {
|
||||||
|
let _ = tx.send(ticket);
|
||||||
|
})?;
|
||||||
|
Some(rx)
|
||||||
|
} else {
|
||||||
|
// Autoarranque: los programas de `~/.config/mirada/autostart`.
|
||||||
|
crate::spawn_autostart(None);
|
||||||
|
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
|
||||||
|
// como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
|
||||||
|
// saltar de VT.
|
||||||
|
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
|
||||||
|
crate::spawn_command(&cmd, None);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
|
||||||
|
println!("[8/8] montando el bucle de eventos …");
|
||||||
|
let mut event_loop: EventLoop<DrmState> =
|
||||||
|
EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
|
||||||
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
|
// Sesión: pausa/activación al conmutar de VT.
|
||||||
|
handle
|
||||||
|
.insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
|
||||||
|
SessionEvent::PauseSession => state.pause_session(),
|
||||||
|
SessionEvent::ActivateSession => state.resume_session(),
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("insert session: {e}"))?;
|
||||||
|
|
||||||
|
// VBlank: el page-flip terminó.
|
||||||
|
handle
|
||||||
|
.insert_source(drm_notifier, |event, _meta, state| match event {
|
||||||
|
DrmEvent::VBlank(_crtc) => {
|
||||||
|
if let Err(e) = state.compositor.frame_submitted() {
|
||||||
|
eprintln!("mirada-compositor · frame_submitted: {e}");
|
||||||
|
}
|
||||||
|
state.pending_flip = false;
|
||||||
|
}
|
||||||
|
DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("insert drm: {e}"))?;
|
||||||
|
|
||||||
|
// Teclado y ratón vía libinput. Guardamos un clon del contexto (es
|
||||||
|
// un manejador con contador de referencias) para suspenderlo y
|
||||||
|
// reanudarlo al conmutar de VT.
|
||||||
|
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||||
|
libinput
|
||||||
|
.udev_assign_seat(&seat_name)
|
||||||
|
.map_err(|()| "libinput: no pude asignar el seat")?;
|
||||||
|
let libinput_handle = libinput.clone();
|
||||||
|
handle
|
||||||
|
.insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
|
||||||
|
state.handle_input(event);
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("insert libinput: {e}"))?;
|
||||||
|
|
||||||
|
// Clientes Wayland nuevos.
|
||||||
|
handle
|
||||||
|
.insert_source(
|
||||||
|
Generic::new(listener, Interest::READ, CalloopMode::Level),
|
||||||
|
|_readiness, listener, state| {
|
||||||
|
while let Some(stream) = listener.accept()? {
|
||||||
|
eprintln!("mirada-compositor · cliente Wayland conectado.");
|
||||||
|
let _ = state
|
||||||
|
.display
|
||||||
|
.handle()
|
||||||
|
.insert_client(stream, Arc::new(ClientState::default()));
|
||||||
|
}
|
||||||
|
Ok(PostAction::Continue)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("insert socket: {e}"))?;
|
||||||
|
|
||||||
|
// Peticiones de los clientes ya conectados.
|
||||||
|
let poll_fd = display.backend().poll_fd().try_clone_to_owned()?;
|
||||||
|
handle
|
||||||
|
.insert_source(
|
||||||
|
Generic::new(poll_fd, Interest::READ, CalloopMode::Level),
|
||||||
|
|_readiness, _fd, state| {
|
||||||
|
let DrmState { display, app, .. } = state;
|
||||||
|
if let Err(e) = display.dispatch_clients(app) {
|
||||||
|
eprintln!("mirada-compositor · dispatch: {e}");
|
||||||
|
}
|
||||||
|
let _ = display.flush_clients();
|
||||||
|
Ok(PostAction::Continue)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("insert display: {e}"))?;
|
||||||
|
|
||||||
|
// Timer de composición + tareas — ~60 Hz.
|
||||||
|
handle
|
||||||
|
.insert_source(Timer::immediate(), |_instant, _meta, state| {
|
||||||
|
state.tick();
|
||||||
|
TimeoutAction::ToDuration(Duration::from_millis(16))
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("insert timer: {e}"))?;
|
||||||
|
|
||||||
|
// Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
|
||||||
|
// El hilo lector del greeter despierta el bucle por este canal.
|
||||||
|
if let Some(rx) = greeter_rx {
|
||||||
|
handle
|
||||||
|
.insert_source(rx, |event, _, state: &mut DrmState| {
|
||||||
|
if let TicketEvent::Msg(ticket) = event {
|
||||||
|
state.app.complete_greeter_handoff(ticket);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("insert greeter: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
|
||||||
|
// compositor solo (0 o sin definir = sin tope). El teclado ya
|
||||||
|
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
|
||||||
|
let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
println!("──────────────────────────────────────────────────");
|
||||||
|
println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
|
||||||
|
println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
|
||||||
|
println!(" Salir: Super+Shift+e · o Ctrl+C en esta TTY.");
|
||||||
|
if timeout_secs > 0 {
|
||||||
|
println!(" Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = DrmState {
|
||||||
|
app,
|
||||||
|
display,
|
||||||
|
drm,
|
||||||
|
compositor,
|
||||||
|
renderer,
|
||||||
|
libinput: libinput_handle,
|
||||||
|
active: true,
|
||||||
|
pending_flip: false,
|
||||||
|
keymap_path,
|
||||||
|
keymap_watch,
|
||||||
|
ctl,
|
||||||
|
start: Instant::now(),
|
||||||
|
last_windows: 0,
|
||||||
|
cursor_id: Id::new(),
|
||||||
|
last_pointer_window: None,
|
||||||
|
output_size: (mode_w as f64, mode_h as f64),
|
||||||
|
};
|
||||||
|
|
||||||
|
let signal = event_loop.get_signal();
|
||||||
|
event_loop
|
||||||
|
.run(None, &mut state, |state| {
|
||||||
|
let timed_out =
|
||||||
|
timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
|
||||||
|
if !state.app.running || timed_out {
|
||||||
|
if timed_out {
|
||||||
|
println!("mirada-compositor · tope de tiempo — cerrando.");
|
||||||
|
}
|
||||||
|
signal.stop();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("el bucle de eventos falló: {e}"))?;
|
||||||
|
|
||||||
|
println!("mirada-compositor · adiós.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-ctl"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada-ctl — control del compositor carmen por línea de comandos (estilo swaymsg/hyprctl): aplica acciones de escritorio y consulta ventanas vía el socket de control de mirada-brain."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada-ctl"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! `mirada-ctl` — el control del compositor carmen por línea de comandos.
|
||||||
|
//!
|
||||||
|
//! Al estilo de `swaymsg` / `hyprctl`: dispara una acción de escritorio o
|
||||||
|
//! consulta el estado, hablando con el Cerebro por su socket de control
|
||||||
|
//! ([`mirada_brain::ctl`]). El Cerebro es la app `mirada`, o
|
||||||
|
//! `mirada-compositor` cuando lleva el Cerebro embebido.
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! mirada-ctl focus-next # cambia el foco
|
||||||
|
//! mirada-ctl focus-window 5 # enfoca una ventana concreta
|
||||||
|
//! mirada-ctl workspace 3 # va al escritorio 3
|
||||||
|
//! mirada-ctl layout grid # fija el modo de teselado
|
||||||
|
//! mirada-ctl windows # lista las ventanas
|
||||||
|
//! mirada-ctl actions # lista las acciones
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use mirada_brain::ctl::{self, CtlReply, CtlRequest, WindowLine};
|
||||||
|
use mirada_brain::DesktopAction;
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
match run(&args) {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(msg) => {
|
||||||
|
eprintln!("mirada-ctl: {msg}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: &[String]) -> Result<(), String> {
|
||||||
|
match args.first().map(String::as_str) {
|
||||||
|
None | Some("-h" | "--help" | "help") => {
|
||||||
|
print_help();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some("actions") => {
|
||||||
|
print_actions();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some("windows") => match request(CtlRequest::ListWindows)? {
|
||||||
|
CtlReply::Windows(ws) => {
|
||||||
|
print_windows(&ws);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
CtlReply::Error(e) => Err(e),
|
||||||
|
CtlReply::Ok => Err("respuesta inesperada del Cerebro".into()),
|
||||||
|
},
|
||||||
|
// Todo lo demás es una acción. `focus-window 5` y `workspace 3`
|
||||||
|
// se unen con `:` a la forma canónica (`focus-window:5`).
|
||||||
|
Some(_) => {
|
||||||
|
let spec = args.join(":");
|
||||||
|
let action: DesktopAction = spec
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("{e}\n lista de acciones: mirada-ctl actions"))?;
|
||||||
|
match request(CtlRequest::Do(action))? {
|
||||||
|
CtlReply::Ok => Ok(()),
|
||||||
|
CtlReply::Error(e) => Err(e),
|
||||||
|
CtlReply::Windows(_) => Err("respuesta inesperada del Cerebro".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manda una petición al Cerebro y devuelve su respuesta.
|
||||||
|
fn request(req: CtlRequest) -> Result<CtlReply, String> {
|
||||||
|
let path = ctl::default_socket_path();
|
||||||
|
ctl::send_request(&path, &req).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"no pude hablar con el Cerebro en {} ({e})\n \
|
||||||
|
¿está corriendo `mirada` o `mirada-compositor`?",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imprime la lista de ventanas, marcando la enfocada con `*`.
|
||||||
|
fn print_windows(windows: &[WindowLine]) {
|
||||||
|
if windows.is_empty() {
|
||||||
|
println!("(no hay ventanas)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for w in windows {
|
||||||
|
let mark = if w.focused { '*' } else { ' ' };
|
||||||
|
// El escritorio 0 es el scratchpad (ventana guardada).
|
||||||
|
let ws = if w.workspace == 0 {
|
||||||
|
"scratch".to_string()
|
||||||
|
} else {
|
||||||
|
w.workspace.to_string()
|
||||||
|
};
|
||||||
|
println!("{mark} id {:<4} esc {:<7} {:<24} {}", w.id, ws, w.app_id, w.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"mirada-ctl — control del compositor carmen\n\
|
||||||
|
\n\
|
||||||
|
USO:\n \
|
||||||
|
mirada-ctl <acción> aplica una acción de escritorio\n \
|
||||||
|
mirada-ctl windows lista las ventanas\n \
|
||||||
|
mirada-ctl actions lista las acciones disponibles\n\
|
||||||
|
\n\
|
||||||
|
EJEMPLOS:\n \
|
||||||
|
mirada-ctl focus-next\n \
|
||||||
|
mirada-ctl focus-window 5\n \
|
||||||
|
mirada-ctl workspace 3\n \
|
||||||
|
mirada-ctl layout grid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_actions() {
|
||||||
|
// Cadena multilínea literal: la indentación de cada línea es la que
|
||||||
|
// se imprime (el `\` tras la comilla se come sólo el primer salto).
|
||||||
|
print!(
|
||||||
|
"\
|
||||||
|
Acciones de mirada-ctl:
|
||||||
|
focus-next mueve el foco a la siguiente ventana
|
||||||
|
focus-prev mueve el foco a la anterior
|
||||||
|
focus-window <id> enfoca la ventana <id> (ver: mirada-ctl windows)
|
||||||
|
move-forward adelanta la ventana enfocada en el teselado
|
||||||
|
move-backward la atrasa
|
||||||
|
close-focused cierra la ventana enfocada
|
||||||
|
toggle-float alterna flotante / teselada la enfocada
|
||||||
|
toggle-fullscreen alterna pantalla completa en la enfocada
|
||||||
|
send-to-scratchpad guarda la ventana enfocada en el scratchpad
|
||||||
|
toggle-scratchpad invoca u oculta la ventana del scratchpad
|
||||||
|
cycle-layout pasa al siguiente modo de teselado
|
||||||
|
layout <modo> master-stack · centered-master · spiral
|
||||||
|
grid · columns · rows · monocle
|
||||||
|
grow-master agranda el área de la ventana maestra
|
||||||
|
shrink-master la encoge
|
||||||
|
inc-master / dec-master nº de ventanas en el área maestra (nmaster)
|
||||||
|
promote-to-master la ventana enfocada al puesto maestro
|
||||||
|
workspace <n> activa el escritorio n (1..9)
|
||||||
|
send-to-workspace <n> manda la enfocada al escritorio n
|
||||||
|
focus-output-next pasa el foco al siguiente monitor
|
||||||
|
quit apaga el compositor
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-greeter"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada-greeter — el greeter de carmen: ventana GPUI de login que autentica con brahman-auth y emite un SessionTicket al compositor por stdout."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada-greeter"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { workspace = true }
|
||||||
|
nahual-theme = { workspace = true }
|
||||||
|
nahual-widget-text-input = { workspace = true }
|
||||||
|
brahman-auth = { path = "../../protocol/brahman-auth" }
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# mirada-greeter
|
||||||
|
|
||||||
|
El greeter (pantalla de login) del escritorio carmen.
|
||||||
|
|
||||||
|
Una ventana GPUI: el compositor `mirada-compositor`, cuando bootea en
|
||||||
|
modo greeter, la arranca como proceso hijo, la compone a pantalla
|
||||||
|
completa (la reconoce por `app_id = "carmen.greeter"`) y le lee el
|
||||||
|
stdout.
|
||||||
|
|
||||||
|
## Flujo
|
||||||
|
|
||||||
|
1. El usuario teclea usuario + contraseña. `Enter` en «usuario» pasa el
|
||||||
|
foco a «contraseña»; `Enter` en «contraseña» autentica.
|
||||||
|
2. La autenticación corre con [`brahman-auth`] en un hilo de fondo (PAM
|
||||||
|
puede demorar ~2 s ante un fallo, no se congela la UI).
|
||||||
|
3. En éxito, el greeter **imprime un `SessionTicket` a stdout** y
|
||||||
|
termina. El compositor parsea esa línea y hace el traspaso a modo
|
||||||
|
sesión sin reiniciar el servidor gráfico.
|
||||||
|
|
||||||
|
La línea de tiquet lleva el prefijo `MIRADA-SESSION-TICKET-v1`; el resto
|
||||||
|
del stdout (logs) se ignora.
|
||||||
|
|
||||||
|
## Backend de autenticación
|
||||||
|
|
||||||
|
| Entorno | Backend |
|
||||||
|
|---|---|
|
||||||
|
| (por defecto) | PAM, servicio `carmen` (`/etc/pam.d/carmen`) |
|
||||||
|
| `MIRADA_GREETER_PAM=<servicio>` | PAM con otro servicio |
|
||||||
|
| `MIRADA_GREETER_MOCK=usuario:secreto` | Mock — credenciales fijas |
|
||||||
|
|
||||||
|
El modo mock sirve para iterar la UI en cajas sin PAM o con el greeter
|
||||||
|
anidado dentro de otro escritorio:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MIRADA_GREETER_MOCK=demo:demo cargo run -p mirada-greeter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integración con el compositor
|
||||||
|
|
||||||
|
El consumo del tiquet ya está cableado. `mirada-compositor --greeter`
|
||||||
|
lanza este greeter, lee su stdout y, al recibir el `SessionTicket`,
|
||||||
|
muta de `BodyMode::Greeter` a `BodyMode::Session` y arranca la sesión
|
||||||
|
del usuario con `setuid`/`setgid` — sin reiniciar el servidor Wayland.
|
||||||
|
Ver el README de `mirada-compositor`, sección **Modo greeter (DM)**.
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
//! `mirada-greeter` — el greeter del escritorio carmen.
|
||||||
|
//!
|
||||||
|
//! Una ventana GPUI de login. El compositor (`mirada-compositor`) la
|
||||||
|
//! arranca como proceso hijo cuando bootea en modo greeter, la compone a
|
||||||
|
//! pantalla completa (la reconoce por `app_id = "carmen.greeter"`) y le
|
||||||
|
//! lee el stdout.
|
||||||
|
//!
|
||||||
|
//! Flujo: el usuario teclea usuario + contraseña, el greeter autentica
|
||||||
|
//! con [`brahman_auth`], y en éxito **imprime un [`SessionTicket`] a
|
||||||
|
//! stdout** y termina. El compositor parsea esa línea, hace el traspaso
|
||||||
|
//! a modo sesión (setuid al usuario + arranque) sin reiniciar el
|
||||||
|
//! servidor gráfico — la «mutación atómica» del DM.
|
||||||
|
//!
|
||||||
|
//! Backend de autenticación (ver [`pick_authenticator`]):
|
||||||
|
//! - por defecto, PAM contra el servicio `carmen`;
|
||||||
|
//! - `MIRADA_GREETER_MOCK="usuario:secreto"` usa el mock, para iterar la
|
||||||
|
//! UI en cajas sin PAM o con el greeter anidado en otro escritorio.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use brahman_auth::{
|
||||||
|
AuthError, Authenticator, MockAuthenticator, PamAuthenticator, SessionTicket, UserInfo,
|
||||||
|
DEFAULT_SERVICE,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
div, prelude::*, px, App, Application, Bounds, Context, Entity, IntoElement, Render,
|
||||||
|
SharedString, Window, WindowBounds, WindowOptions,
|
||||||
|
};
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
use nahual_widget_text_input::{TextInput, TextInputEvent};
|
||||||
|
|
||||||
|
/// `app_id` con el que el compositor reconoce y compone el greeter.
|
||||||
|
const GREETER_APP_ID: &str = "carmen.greeter";
|
||||||
|
|
||||||
|
/// Autenticador compartible entre el hilo de UI y el de fondo.
|
||||||
|
type DynAuth = Arc<dyn Authenticator + Send + Sync>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Application::new().run(|cx: &mut App| {
|
||||||
|
Theme::install_default(cx);
|
||||||
|
let auth = pick_authenticator();
|
||||||
|
let bounds = Bounds::centered(None, gpui::size(px(960.0), px(600.0)), cx);
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
titlebar: None,
|
||||||
|
app_id: Some(GREETER_APP_ID.into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|window, cx| cx.new(|cx| Greeter::new(auth, window, cx)),
|
||||||
|
)
|
||||||
|
.expect("abrir la ventana del greeter");
|
||||||
|
cx.activate(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elige el backend de autenticación según el entorno.
|
||||||
|
fn pick_authenticator() -> DynAuth {
|
||||||
|
// Modo dev: credenciales fijas, sin tocar PAM.
|
||||||
|
if let Ok(spec) = std::env::var("MIRADA_GREETER_MOCK") {
|
||||||
|
if let Some((user, secret)) = spec.split_once(':') {
|
||||||
|
eprintln!("mirada-greeter · backend mock (usuario «{user}»)");
|
||||||
|
return Arc::new(MockAuthenticator::new().with_user(user, secret));
|
||||||
|
}
|
||||||
|
eprintln!("mirada-greeter · MIRADA_GREETER_MOCK mal formado (falta «:»), ignorado");
|
||||||
|
}
|
||||||
|
// Camino real: PAM. Servicio sobreescribible con `MIRADA_GREETER_PAM`.
|
||||||
|
let service =
|
||||||
|
std::env::var("MIRADA_GREETER_PAM").unwrap_or_else(|_| DEFAULT_SERVICE.to_string());
|
||||||
|
eprintln!("mirada-greeter · backend PAM (servicio «{service}»)");
|
||||||
|
Arc::new(PamAuthenticator::new(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estado del intento de login en curso.
|
||||||
|
enum Status {
|
||||||
|
/// Esperando que el usuario teclee.
|
||||||
|
Idle,
|
||||||
|
/// Autenticación en vuelo (en el hilo de fondo).
|
||||||
|
Authenticating,
|
||||||
|
/// Último intento falló; el mensaje es para mostrar.
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Greeter {
|
||||||
|
auth: DynAuth,
|
||||||
|
username: Entity<TextInput>,
|
||||||
|
password: Entity<TextInput>,
|
||||||
|
status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Greeter {
|
||||||
|
fn new(auth: DynAuth, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||||
|
|
||||||
|
let username = cx.new(|cx| TextInput::new("", cx).with_placeholder("usuario"));
|
||||||
|
let password = cx.new(|cx| {
|
||||||
|
TextInput::new("", cx)
|
||||||
|
.with_placeholder("contraseña")
|
||||||
|
.with_mask()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter en «usuario» pasa el foco a «contraseña».
|
||||||
|
cx.subscribe_in(&username, window, |this, _u, ev, window, cx| {
|
||||||
|
if let TextInputEvent::Confirmed(_) = ev {
|
||||||
|
this.password.read(cx).request_focus(window);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Enter en «contraseña» dispara la autenticación.
|
||||||
|
cx.subscribe_in(&password, window, |this, _p, ev, window, cx| {
|
||||||
|
if let TextInputEvent::Confirmed(_) = ev {
|
||||||
|
this.submit(window, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
// Foco inicial en «usuario».
|
||||||
|
username.read(cx).request_focus(window);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
auth,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
status: Status::Idle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valida el formulario y lanza la autenticación en el hilo de fondo
|
||||||
|
/// (PAM puede tardar — `pam_unix` demora ~2 s ante un fallo).
|
||||||
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if matches!(self.status, Status::Authenticating) {
|
||||||
|
return; // intento ya en curso
|
||||||
|
}
|
||||||
|
let user = self.username.read(cx).text().trim().to_string();
|
||||||
|
let secret = self.password.read(cx).text().to_string();
|
||||||
|
if user.is_empty() {
|
||||||
|
self.status = Status::Failed("ingresá un usuario".into());
|
||||||
|
self.username.read(cx).request_focus(window);
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status = Status::Authenticating;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
let auth = Arc::clone(&self.auth);
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move { auth.authenticate(&user, &secret) })
|
||||||
|
.await;
|
||||||
|
let _ = this.update(cx, |me, cx| me.finish(result, cx));
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procesa el resultado de la autenticación.
|
||||||
|
fn finish(&mut self, result: Result<UserInfo, AuthError>, cx: &mut Context<Self>) {
|
||||||
|
match result {
|
||||||
|
Ok(user) => {
|
||||||
|
// El compositor lee esta línea del stdout del greeter.
|
||||||
|
emit_ticket(&SessionTicket::new(user));
|
||||||
|
cx.quit();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.status = Status::Failed(e.to_string());
|
||||||
|
// Limpia la contraseña; el foco ya está en ese campo.
|
||||||
|
self.password.update(cx, |p, cx| p.set_text("", cx));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Greeter {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let (status_msg, status_color) = match &self.status {
|
||||||
|
Status::Idle => (SharedString::default(), theme.fg_muted),
|
||||||
|
Status::Authenticating => (SharedString::from("verificando…"), theme.fg_muted),
|
||||||
|
Status::Failed(m) => (SharedString::from(m.clone()), theme.accent_destructive()),
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.w(px(320.0))
|
||||||
|
.p(px(28.0))
|
||||||
|
.bg(theme.bg_panel)
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border)
|
||||||
|
.rounded(px(12.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(22.0))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child("carmen"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child("iniciá tu sesión"),
|
||||||
|
)
|
||||||
|
.child(caption(&theme, "usuario"))
|
||||||
|
.child(self.username.clone())
|
||||||
|
.child(caption(&theme, "contraseña"))
|
||||||
|
.child(self.password.clone())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(px(16.0))
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(status_color)
|
||||||
|
.child(status_msg),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Etiqueta pequeña sobre un campo del formulario.
|
||||||
|
fn caption(theme: &Theme, text: &'static str) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imprime el tiquet a stdout y fuerza el flush antes de terminar.
|
||||||
|
fn emit_ticket(ticket: &SessionTicket) {
|
||||||
|
println!("{}", ticket.to_line());
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-launcher"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada-launcher — lanzador de aplicaciones para carmen: escanea los .desktop del sistema, los lista en la terminal y lanza el que elijas. Sin dependencias; pensado para correr en una terminal pequeña que el compositor abre con un atajo."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada-launcher"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
//! `mirada-launcher` — un lanzador de aplicaciones para carmen.
|
||||||
|
//!
|
||||||
|
//! Escanea los archivos `.desktop` del sistema (el estándar XDG), los
|
||||||
|
//! lista en la terminal y lanza el que elijas. No tiene dependencias: la
|
||||||
|
//! interfaz es una lista numerada que se filtra escribiendo.
|
||||||
|
//!
|
||||||
|
//! Pensado para correr dentro de una terminal pequeña que el compositor
|
||||||
|
//! abre con un atajo — p. ej. atando `Super+d` a
|
||||||
|
//! `spawn:foot -e mirada-launcher` en el keymap de mirada. Al elegir una
|
||||||
|
//! aplicación, la lanza y termina (la terminal se cierra sola); el
|
||||||
|
//! programa lanzado queda corriendo, reparentado a init.
|
||||||
|
//!
|
||||||
|
//! También sirve suelto: `mirada-launcher` en cualquier terminal.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Una aplicación lista para lanzar, sacada de un `.desktop`.
|
||||||
|
struct DesktopApp {
|
||||||
|
/// Nombre visible (`Name=`).
|
||||||
|
name: String,
|
||||||
|
/// Comando a ejecutar, ya sin los códigos de campo (`%u`, `%F`…).
|
||||||
|
exec: String,
|
||||||
|
/// `true` si la app necesita una terminal (`Terminal=true`).
|
||||||
|
needs_terminal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut apps = scan_apps();
|
||||||
|
apps.sort_by_key(|a| a.name.to_lowercase());
|
||||||
|
if apps.is_empty() {
|
||||||
|
eprintln!("mirada-launcher · no encontré ninguna aplicación .desktop.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
run_ui(&apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Escaneo de los .desktop
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Recorre los directorios XDG de aplicaciones y devuelve las que se
|
||||||
|
/// pueden lanzar. Un `.desktop` de un directorio de mayor prioridad
|
||||||
|
/// tapa a otro con el mismo nombre de archivo en uno de menor.
|
||||||
|
fn scan_apps() -> Vec<DesktopApp> {
|
||||||
|
let mut apps = Vec::new();
|
||||||
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
for dir in application_dirs() {
|
||||||
|
collect_desktop_files(&dir, &dir, &mut seen, &mut apps);
|
||||||
|
}
|
||||||
|
apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Los directorios `applications/` del estándar XDG, en orden de
|
||||||
|
/// prioridad: primero el del usuario, luego los del sistema.
|
||||||
|
fn application_dirs() -> Vec<PathBuf> {
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
let data_home = std::env::var_os("XDG_DATA_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|p| p.is_absolute())
|
||||||
|
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")));
|
||||||
|
if let Some(home) = data_home {
|
||||||
|
dirs.push(home.join("applications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_dirs = std::env::var("XDG_DATA_DIRS")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| "/usr/local/share:/usr/share".to_string());
|
||||||
|
for d in data_dirs.split(':').filter(|s| !s.is_empty()) {
|
||||||
|
dirs.push(PathBuf::from(d).join("applications"));
|
||||||
|
}
|
||||||
|
dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recoge los `.desktop` de `dir` (y subdirectorios) sin repetir id.
|
||||||
|
fn collect_desktop_files(
|
||||||
|
root: &PathBuf,
|
||||||
|
dir: &PathBuf,
|
||||||
|
seen: &mut HashSet<String>,
|
||||||
|
apps: &mut Vec<DesktopApp>,
|
||||||
|
) {
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_desktop_files(root, &path, seen, apps);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.extension().and_then(|e| e.to_str()) != Some("desktop") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// El id XDG: la ruta relativa al directorio raíz, con `/` → `-`.
|
||||||
|
let id = path
|
||||||
|
.strip_prefix(root)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('/', "-");
|
||||||
|
if !seen.insert(id) {
|
||||||
|
continue; // ya lo tapó un directorio de más prioridad
|
||||||
|
}
|
||||||
|
if let Ok(text) = std::fs::read_to_string(&path) {
|
||||||
|
if let Some(app) = parse_desktop(&text) {
|
||||||
|
apps.push(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrae una [`DesktopApp`] del texto de un `.desktop`. `None` si no es
|
||||||
|
/// una aplicación lanzable o está marcada para no mostrarse.
|
||||||
|
fn parse_desktop(text: &str) -> Option<DesktopApp> {
|
||||||
|
let mut in_entry = false;
|
||||||
|
let (mut name, mut exec, mut kind) = (None, None, None);
|
||||||
|
let (mut no_display, mut hidden, mut terminal) = (false, false, false);
|
||||||
|
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with('[') {
|
||||||
|
// Sólo nos interesa el grupo principal; otros (acciones,
|
||||||
|
// etc.) se ignoran.
|
||||||
|
in_entry = line == "[Desktop Entry]";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !in_entry || line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((key, value)) = line.split_once('=') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = value.trim();
|
||||||
|
match key.trim() {
|
||||||
|
"Name" => name = Some(value.to_string()),
|
||||||
|
"Exec" => exec = Some(value.to_string()),
|
||||||
|
"Type" => kind = Some(value.to_string()),
|
||||||
|
"NoDisplay" => no_display = value == "true",
|
||||||
|
"Hidden" => hidden = value == "true",
|
||||||
|
"Terminal" => terminal = value == "true",
|
||||||
|
_ => {} // Name[es], Icon, Categories…: no los usamos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if no_display || hidden {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if kind.as_deref() != Some("Application") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = name?;
|
||||||
|
let exec = strip_field_codes(&exec?);
|
||||||
|
if name.is_empty() || exec.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(DesktopApp { name, exec, needs_terminal: terminal })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quita los códigos de campo de un `Exec` de `.desktop` (`%u`, `%F`,
|
||||||
|
/// `%i`…), que sólo tienen sentido al abrir archivos. `%%` queda en `%`.
|
||||||
|
fn strip_field_codes(exec: &str) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut chars = exec.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '%' {
|
||||||
|
// `%%` es un `%` literal; cualquier otro `%x` es un código de
|
||||||
|
// campo y se descarta entero.
|
||||||
|
if let Some('%') = chars.next() {
|
||||||
|
out.push('%');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Interfaz de terminal
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Cuántas aplicaciones se listan como mucho de una vez.
|
||||||
|
const MAX_SHOWN: usize = 40;
|
||||||
|
|
||||||
|
/// El bucle de la interfaz: muestra la lista, lee una línea y según sea
|
||||||
|
/// un número lanza, texto filtra, o vacía sale.
|
||||||
|
fn run_ui(apps: &[DesktopApp]) {
|
||||||
|
let mut filter = String::new();
|
||||||
|
loop {
|
||||||
|
let needle = filter.to_lowercase();
|
||||||
|
let matches: Vec<&DesktopApp> = apps
|
||||||
|
.iter()
|
||||||
|
.filter(|a| needle.is_empty() || a.name.to_lowercase().contains(&needle))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Limpia la pantalla y dibuja la lista.
|
||||||
|
print!("\x1b[2J\x1b[H");
|
||||||
|
if filter.is_empty() {
|
||||||
|
println!("mirada-launcher · {} aplicaciones", matches.len());
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"mirada-launcher · {} de {} · filtro «{filter}»",
|
||||||
|
matches.len(),
|
||||||
|
apps.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
if matches.is_empty() {
|
||||||
|
println!(" (sin coincidencias)");
|
||||||
|
}
|
||||||
|
for (i, a) in matches.iter().take(MAX_SHOWN).enumerate() {
|
||||||
|
println!(" {:>2} {}", i + 1, a.name);
|
||||||
|
}
|
||||||
|
if matches.len() > MAX_SHOWN {
|
||||||
|
println!(" … y {} más — afina el filtro", matches.len() - MAX_SHOWN);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!(" nº = lanzar · texto = filtrar · Enter vacío = salir");
|
||||||
|
print!("> ");
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
if io::stdin().read_line(&mut line).unwrap_or(0) == 0 {
|
||||||
|
return; // fin de entrada (Ctrl+D)
|
||||||
|
}
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ¿Un número? Lanza esa entrada de la lista visible.
|
||||||
|
if let Ok(n) = line.parse::<usize>() {
|
||||||
|
if (1..=matches.len().min(MAX_SHOWN)).contains(&n) {
|
||||||
|
launch(matches[n - 1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue; // número fuera de rango: vuelve a pedir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texto: es un filtro nuevo. Si deja una sola, lánzala directo.
|
||||||
|
filter = line.to_string();
|
||||||
|
let needle = filter.to_lowercase();
|
||||||
|
let now: Vec<&DesktopApp> = apps
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.name.to_lowercase().contains(&needle))
|
||||||
|
.collect();
|
||||||
|
if now.len() == 1 {
|
||||||
|
launch(now[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lanza la aplicación elegida como proceso hijo y devuelve. Hereda el
|
||||||
|
/// entorno —`WAYLAND_DISPLAY` incluido—; al terminar el lanzador, el
|
||||||
|
/// proceso queda corriendo, reparentado a init.
|
||||||
|
fn launch(app: &DesktopApp) {
|
||||||
|
let cmd = if app.needs_terminal {
|
||||||
|
format!("foot -e {}", app.exec)
|
||||||
|
} else {
|
||||||
|
app.exec.clone()
|
||||||
|
};
|
||||||
|
print!("\x1b[2J\x1b[H");
|
||||||
|
println!("mirada-launcher · lanzando «{}» …", app.name);
|
||||||
|
match std::process::Command::new("sh").arg("-c").arg(&cmd).spawn() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("mirada-launcher · no pude lanzar «{cmd}»: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-portal"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada-portal — backend xdg-desktop-portal de carmen: implementa org.freedesktop.impl.portal.Settings y publica el tema activo de nahual (claro/oscuro + acento + contraste) a GTK, Qt, Firefox y Chromium por protocolo."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada-portal"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zbus = { workspace = true }
|
||||||
|
notify = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# mirada-portal
|
||||||
|
|
||||||
|
Backend de `xdg-desktop-portal` para el escritorio **carmen**. Implementa
|
||||||
|
`org.freedesktop.impl.portal.Settings` y publica un único namespace:
|
||||||
|
`org.freedesktop.appearance`.
|
||||||
|
|
||||||
|
## Qué resuelve
|
||||||
|
|
||||||
|
GTK y Qt leen su configuración de sitios incompatibles entre sí. Pero
|
||||||
|
**ambos** —además de Firefox y Chromium— consultan el portal de
|
||||||
|
FreeDesktop para saber:
|
||||||
|
|
||||||
|
- `color-scheme` — claro (`2`) u oscuro (`1`),
|
||||||
|
- `accent-color` — el color de acento como `(ddd)` RGB,
|
||||||
|
- `contrast` — contraste alto (`1`) o normal (`0`).
|
||||||
|
|
||||||
|
`mirada-portal` responde esas tres claves a partir del tema activo de
|
||||||
|
`nahual` y, cuando el tema cambia, emite `SettingChanged`: todo el
|
||||||
|
ecosistema voltea en vivo, **sin tocar un solo archivo de config de las
|
||||||
|
apps**.
|
||||||
|
|
||||||
|
## Fuente del tema
|
||||||
|
|
||||||
|
El daemon lee `$XDG_CONFIG_HOME/nahual/theme` (el archivo que persiste
|
||||||
|
`nahual-theme`, con el nombre del preset activo) y lo vigila con
|
||||||
|
`notify`. La traducción nombre → hechos del portal está en
|
||||||
|
[`src/theme_facts.rs`], que espeja `nahual_theme::Theme::all()` sin
|
||||||
|
enlazar GPUI.
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
Esto es el **backend** del portal. El frontend genérico
|
||||||
|
`xdg-desktop-portal` (paquete agnóstico, liviano) enruta las llamadas de
|
||||||
|
las apps hacia este backend según el archivo `mirada.portal`. No hay que
|
||||||
|
implementar el frontend.
|
||||||
|
|
||||||
|
## Instalación de los archivos de `data/`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
install -Dm644 data/mirada.portal \
|
||||||
|
/usr/share/xdg-desktop-portal/portals/mirada.portal
|
||||||
|
install -Dm644 data/mirada-portals.conf \
|
||||||
|
/usr/share/xdg-desktop-portal/mirada-portals.conf
|
||||||
|
install -Dm644 data/org.freedesktop.impl.portal.desktop.mirada.service \
|
||||||
|
/usr/share/dbus-1/services/org.freedesktop.impl.portal.desktop.mirada.service
|
||||||
|
install -Dm755 target/release/mirada-portal /usr/bin/mirada-portal
|
||||||
|
```
|
||||||
|
|
||||||
|
El frontend casa `UseIn=mirada` contra `XDG_CURRENT_DESKTOP`, así que
|
||||||
|
carmen debe exportar `XDG_CURRENT_DESKTOP=mirada`. Alternativamente, el
|
||||||
|
`mirada-portals.conf` lo fuerza con `default=mirada`.
|
||||||
|
|
||||||
|
`mirada-portal` se puede arrancar desde `~/.config/mirada/autostart` o
|
||||||
|
dejar que el frontend lo active por D-Bus (de ahí el `.service`).
|
||||||
|
|
||||||
|
## Smoke test (sin frontend ni apps GTK)
|
||||||
|
|
||||||
|
Con un bus de sesión vivo, el backend se puede interrogar directo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
busctl --user introspect org.freedesktop.impl.portal.desktop.mirada \
|
||||||
|
/org/freedesktop/portal/desktop
|
||||||
|
busctl --user call org.freedesktop.impl.portal.desktop.mirada \
|
||||||
|
/org/freedesktop/portal/desktop \
|
||||||
|
org.freedesktop.impl.portal.Settings ReadAll as 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Cambiar `~/.config/nahual/theme` debe disparar una señal `SettingChanged`
|
||||||
|
(observable con `busctl --user monitor`).
|
||||||
|
|
||||||
|
## Límite conocido (v1)
|
||||||
|
|
||||||
|
El portal `org.freedesktop.appearance` sólo lleva claro/oscuro + acento +
|
||||||
|
contraste. **No** lleva la paleta completa de `nahual`. Para recolorear
|
||||||
|
GTK/Qt a los colores exactos del tema hace falta, además, inyección de
|
||||||
|
entorno + CSS generado en el `spawn` de carmen — siguiente paso del plan.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[preferred]
|
||||||
|
default=mirada
|
||||||
|
org.freedesktop.impl.portal.Settings=mirada
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[portal]
|
||||||
|
DBusName=org.freedesktop.impl.portal.desktop.mirada
|
||||||
|
Interfaces=org.freedesktop.impl.portal.Settings
|
||||||
|
UseIn=mirada
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.impl.portal.desktop.mirada
|
||||||
|
Exec=/usr/bin/mirada-portal
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
//! `mirada-portal` — backend de `xdg-desktop-portal` para el escritorio
|
||||||
|
//! carmen.
|
||||||
|
//!
|
||||||
|
//! Implementa la interfaz `org.freedesktop.impl.portal.Settings` y
|
||||||
|
//! publica un único namespace: `org.freedesktop.appearance`. Con eso,
|
||||||
|
//! GTK4/libadwaita, Qt6, Firefox y Chromium leen del sistema —
|
||||||
|
//! **por protocolo, sin tocar sus archivos de config** — si el
|
||||||
|
//! escritorio está en modo claro u oscuro, su color de acento y si pide
|
||||||
|
//! contraste alto. Cuando el tema de `nahual` cambia, el portal emite
|
||||||
|
//! `SettingChanged` y todas esas apps voltean en vivo.
|
||||||
|
//!
|
||||||
|
//! Fuente del tema: el archivo que persiste `nahual-theme`
|
||||||
|
//! (`$XDG_CONFIG_HOME/nahual/theme`, contiene el nombre del preset
|
||||||
|
//! activo). El portal lo vigila con `notify` y reexpone sus hechos —
|
||||||
|
//! ver [`theme_facts`].
|
||||||
|
//!
|
||||||
|
//! Este crate es el **backend** del portal: el frontend genérico
|
||||||
|
//! `xdg-desktop-portal` lo enruta vía el archivo `mirada.portal`. Ver
|
||||||
|
//! el README para la instalación de los archivos de `data/`.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use zbus::zvariant::{OwnedValue, Value};
|
||||||
|
use zbus::{fdo, interface, SignalContext};
|
||||||
|
|
||||||
|
mod theme_facts;
|
||||||
|
use theme_facts::ThemeFacts;
|
||||||
|
|
||||||
|
/// Nombre de bus del backend. El patrón `org.freedesktop.impl.portal.
|
||||||
|
/// desktop.<id>` es el que espera el frontend `xdg-desktop-portal`; el
|
||||||
|
/// `<id>` (`mirada`) tiene que coincidir con el `DBusName` del archivo
|
||||||
|
/// `mirada.portal`.
|
||||||
|
const BUS_NAME: &str = "org.freedesktop.impl.portal.desktop.mirada";
|
||||||
|
|
||||||
|
/// Ruta de objeto canónica de los portales del escritorio.
|
||||||
|
const OBJ_PATH: &str = "/org/freedesktop/portal/desktop";
|
||||||
|
|
||||||
|
/// Único namespace que servimos. El estándar moderno que leen GTK, Qt,
|
||||||
|
/// Firefox y Chromium para claro/oscuro + acento.
|
||||||
|
const APPEARANCE_NS: &str = "org.freedesktop.appearance";
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
init_tracing();
|
||||||
|
info!("mirada-portal: arrancando backend org.freedesktop.impl.portal.Settings");
|
||||||
|
|
||||||
|
let theme_path = theme_config_path();
|
||||||
|
let initial = read_facts(theme_path.as_deref());
|
||||||
|
info!(
|
||||||
|
?theme_path,
|
||||||
|
color_scheme = initial.color_scheme(),
|
||||||
|
contrast = initial.contrast(),
|
||||||
|
"tema inicial resuelto"
|
||||||
|
);
|
||||||
|
|
||||||
|
let facts = Arc::new(Mutex::new(initial));
|
||||||
|
let portal = SettingsPortal {
|
||||||
|
facts: Arc::clone(&facts),
|
||||||
|
};
|
||||||
|
|
||||||
|
// El portal vive en el bus de **sesión** (no el de sistema): es un
|
||||||
|
// servicio del escritorio del usuario, no del sistema.
|
||||||
|
let conn_result = zbus::connection::Builder::session()
|
||||||
|
.and_then(|b| b.name(BUS_NAME))
|
||||||
|
.and_then(|b| b.serve_at(OBJ_PATH, portal));
|
||||||
|
|
||||||
|
match conn_result {
|
||||||
|
Ok(builder) => match builder.build().await {
|
||||||
|
Ok(conn) => {
|
||||||
|
info!(name = BUS_NAME, "name adquirido en el bus de sesión");
|
||||||
|
run(conn, facts, theme_path).await
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(?e, "no se pudo construir la conexión D-Bus — modo idle");
|
||||||
|
wait_for_term().await
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
warn!(?e, "builder D-Bus falló (¿hay bus de sesión?) — modo idle");
|
||||||
|
wait_for_term().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conectado al bus: monta el watcher del tema y espera la señal de
|
||||||
|
/// término. El watcher se guarda en `_watcher` para que no se dropee
|
||||||
|
/// (al dropearse dejaría de vigilar).
|
||||||
|
async fn run(
|
||||||
|
conn: zbus::Connection,
|
||||||
|
facts: Arc<Mutex<ThemeFacts>>,
|
||||||
|
theme_path: Option<PathBuf>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let _watcher = match &theme_path {
|
||||||
|
Some(path) => match spawn_theme_watcher(conn.clone(), Arc::clone(&facts), path.clone()) {
|
||||||
|
Ok(w) => Some(w),
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
?e,
|
||||||
|
"watcher del tema no disponible — el portal no actualizará en vivo"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
warn!("sin ruta de config de tema — el portal sirve un valor fijo");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wait_for_term().await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Interfaz D-Bus: org.freedesktop.impl.portal.Settings
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
struct SettingsPortal {
|
||||||
|
/// Hechos del tema activo. El watcher los reescribe cuando cambia.
|
||||||
|
facts: Arc<Mutex<ThemeFacts>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interface(name = "org.freedesktop.impl.portal.Settings")]
|
||||||
|
impl SettingsPortal {
|
||||||
|
/// Versión de la interfaz impl. `ReadOne` se agregó en la 2.
|
||||||
|
#[zbus(property, name = "version")]
|
||||||
|
fn version(&self) -> u32 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ReadAll(namespaces) -> a{sa{sv}}`. Los `namespaces` son patrones
|
||||||
|
/// (sufijo `*` = prefijo); lista vacía = todos. Sólo respondemos
|
||||||
|
/// `org.freedesktop.appearance`.
|
||||||
|
async fn read_all(
|
||||||
|
&self,
|
||||||
|
namespaces: Vec<String>,
|
||||||
|
) -> fdo::Result<HashMap<String, HashMap<String, OwnedValue>>> {
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
if namespace_requested(&namespaces, APPEARANCE_NS) {
|
||||||
|
let facts = *self.facts.lock().unwrap();
|
||||||
|
out.insert(APPEARANCE_NS.to_string(), appearance_map(&facts)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `ReadOne(namespace, key) -> v`. Lee un único valor.
|
||||||
|
async fn read_one(&self, namespace: String, key: String) -> fdo::Result<OwnedValue> {
|
||||||
|
let facts = *self.facts.lock().unwrap();
|
||||||
|
lookup(&facts, &namespace, &key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Read(namespace, key) -> v`. Deprecado a favor de `ReadOne` desde
|
||||||
|
/// la versión 2 del portal, pero apps viejas lo siguen llamando.
|
||||||
|
async fn read(&self, namespace: String, key: String) -> fdo::Result<OwnedValue> {
|
||||||
|
let facts = *self.facts.lock().unwrap();
|
||||||
|
lookup(&facts, &namespace, &key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `SettingChanged(namespace, key, value)`. Lo emite el watcher
|
||||||
|
/// cuando el tema persistido cambia.
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn setting_changed(
|
||||||
|
ctxt: &SignalContext<'_>,
|
||||||
|
namespace: &str,
|
||||||
|
key: &str,
|
||||||
|
value: Value<'_>,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mapeo tema → valores del portal
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Construye el mapa `a{sv}` del namespace `org.freedesktop.appearance`.
|
||||||
|
fn appearance_map(facts: &ThemeFacts) -> fdo::Result<HashMap<String, OwnedValue>> {
|
||||||
|
Ok(HashMap::from([
|
||||||
|
(
|
||||||
|
"color-scheme".to_string(),
|
||||||
|
into_owned(Value::U32(facts.color_scheme()))?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contrast".to_string(),
|
||||||
|
into_owned(Value::U32(facts.contrast()))?,
|
||||||
|
),
|
||||||
|
("accent-color".to_string(), into_owned(accent_value(facts))?),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resuelve una clave concreta dentro de `org.freedesktop.appearance`.
|
||||||
|
fn lookup(facts: &ThemeFacts, namespace: &str, key: &str) -> fdo::Result<OwnedValue> {
|
||||||
|
if namespace != APPEARANCE_NS {
|
||||||
|
return Err(fdo::Error::Failed(format!(
|
||||||
|
"namespace no servido por mirada-portal: {namespace}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let value = match key {
|
||||||
|
"color-scheme" => Value::U32(facts.color_scheme()),
|
||||||
|
"contrast" => Value::U32(facts.contrast()),
|
||||||
|
"accent-color" => accent_value(facts),
|
||||||
|
other => {
|
||||||
|
return Err(fdo::Error::Failed(format!(
|
||||||
|
"clave desconocida en {APPEARANCE_NS}: {other}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
into_owned(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El acento como structure `(ddd)` — tres dobles RGB en 0..1.
|
||||||
|
fn accent_value(facts: &ThemeFacts) -> Value<'static> {
|
||||||
|
let (r, g, b) = facts.accent_rgb();
|
||||||
|
Value::from((r, g, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Value` → `OwnedValue`. Sólo falla con valores que llevan fds; los
|
||||||
|
/// nuestros (enteros y dobles) nunca lo hacen.
|
||||||
|
fn into_owned(value: Value<'_>) -> fdo::Result<OwnedValue> {
|
||||||
|
OwnedValue::try_from(value).map_err(|e| fdo::Error::Failed(format!("zvariant: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ¿El patrón de namespaces de un `ReadAll` pide `ns`? Lista vacía =
|
||||||
|
/// todos. Un patrón con sufijo `*` matchea por prefijo; sino, exacto.
|
||||||
|
fn namespace_requested(patterns: &[String], ns: &str) -> bool {
|
||||||
|
if patterns.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
patterns.iter().any(|p| match p.strip_suffix('*') {
|
||||||
|
Some(prefix) => ns.starts_with(prefix),
|
||||||
|
None => p == ns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Watcher del tema persistido
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Vigila el archivo de tema de `nahual`; cuando cambia, recomputa los
|
||||||
|
/// hechos y emite `SettingChanged`. Devuelve el watcher, que el caller
|
||||||
|
/// debe mantener vivo.
|
||||||
|
fn spawn_theme_watcher(
|
||||||
|
conn: zbus::Connection,
|
||||||
|
facts: Arc<Mutex<ThemeFacts>>,
|
||||||
|
path: PathBuf,
|
||||||
|
) -> notify::Result<notify::RecommendedWatcher> {
|
||||||
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
|
||||||
|
// Canal acotado: el callback de notify (en su propio hilo) sólo
|
||||||
|
// hace `try_send`; si el buffer está lleno ya hay un evento
|
||||||
|
// pendiente y da igual perder éste — coalescencia natural.
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(8);
|
||||||
|
let mut watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
|
||||||
|
if res.is_ok() {
|
||||||
|
let _ = tx.try_send(());
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Vigilamos el **directorio padre**: así captamos también la
|
||||||
|
// creación del archivo si aún no existe.
|
||||||
|
let watch_target = path.parent().unwrap_or(&path).to_path_buf();
|
||||||
|
watcher.watch(&watch_target, RecursiveMode::NonRecursive)?;
|
||||||
|
info!(dir = ?watch_target, "vigilando el directorio del tema");
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while rx.recv().await.is_some() {
|
||||||
|
let fresh = read_facts(Some(&path));
|
||||||
|
let changed = {
|
||||||
|
let mut guard = facts.lock().unwrap();
|
||||||
|
let differs = *guard != fresh;
|
||||||
|
*guard = fresh;
|
||||||
|
differs
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
info!(
|
||||||
|
color_scheme = fresh.color_scheme(),
|
||||||
|
contrast = fresh.contrast(),
|
||||||
|
"el tema cambió — emitiendo SettingChanged"
|
||||||
|
);
|
||||||
|
if let Err(e) = emit_appearance_changed(&conn, &fresh).await {
|
||||||
|
warn!(?e, "no se pudo emitir SettingChanged");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emite `SettingChanged` para las tres claves de `appearance`.
|
||||||
|
async fn emit_appearance_changed(conn: &zbus::Connection, facts: &ThemeFacts) -> zbus::Result<()> {
|
||||||
|
let ctxt = SignalContext::new(conn, OBJ_PATH)?;
|
||||||
|
SettingsPortal::setting_changed(
|
||||||
|
&ctxt,
|
||||||
|
APPEARANCE_NS,
|
||||||
|
"color-scheme",
|
||||||
|
Value::U32(facts.color_scheme()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
SettingsPortal::setting_changed(
|
||||||
|
&ctxt,
|
||||||
|
APPEARANCE_NS,
|
||||||
|
"contrast",
|
||||||
|
Value::U32(facts.contrast()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
SettingsPortal::setting_changed(&ctxt, APPEARANCE_NS, "accent-color", accent_value(facts))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Lectura del tema persistido
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Lee el nombre de tema del archivo y resuelve sus hechos. Si el
|
||||||
|
/// archivo falta o está vacío, asume `Nebula` — el default de
|
||||||
|
/// `nahual_theme::install_default`.
|
||||||
|
fn read_facts(path: Option<&Path>) -> ThemeFacts {
|
||||||
|
let name = path
|
||||||
|
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| "Nebula".to_string());
|
||||||
|
theme_facts::facts_for(&name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ruta del archivo donde `nahual-theme` persiste el nombre del tema
|
||||||
|
/// activo. Réplica de `nahual_theme::config_path()` — `mirada-portal`
|
||||||
|
/// no enlaza GPUI, así que no puede llamarla directamente. Convención
|
||||||
|
/// XDG: `$XDG_CONFIG_HOME/nahual/theme`, sino `$HOME/.config/...`.
|
||||||
|
fn theme_config_path() -> Option<PathBuf> {
|
||||||
|
let base = std::env::var("XDG_CONFIG_HOME")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var("HOME")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|h| PathBuf::from(h).join(".config"))
|
||||||
|
})?;
|
||||||
|
Some(base.join("nahual").join("theme"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Plomería
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async fn wait_for_term() -> anyhow::Result<()> {
|
||||||
|
let mut term = signal(SignalKind::terminate())?;
|
||||||
|
let mut int_ = signal(SignalKind::interrupt())?;
|
||||||
|
tokio::select! {
|
||||||
|
_ = term.recv() => info!("SIGTERM"),
|
||||||
|
_ = int_.recv() => info!("SIGINT"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("mirada_portal=info"));
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(filter)
|
||||||
|
.with_target(true)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn namespace_empty_matches_all() {
|
||||||
|
assert!(namespace_requested(&[], APPEARANCE_NS));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn namespace_exact_match() {
|
||||||
|
assert!(namespace_requested(
|
||||||
|
&[APPEARANCE_NS.to_string()],
|
||||||
|
APPEARANCE_NS
|
||||||
|
));
|
||||||
|
assert!(!namespace_requested(
|
||||||
|
&["org.example".to_string()],
|
||||||
|
APPEARANCE_NS
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn namespace_wildcard_prefix() {
|
||||||
|
assert!(namespace_requested(
|
||||||
|
&["org.freedesktop.*".to_string()],
|
||||||
|
APPEARANCE_NS
|
||||||
|
));
|
||||||
|
assert!(!namespace_requested(
|
||||||
|
&["org.gnome.*".to_string()],
|
||||||
|
APPEARANCE_NS
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn appearance_map_has_three_keys() {
|
||||||
|
let facts = theme_facts::facts_for("Nebula");
|
||||||
|
let m = appearance_map(&facts).unwrap();
|
||||||
|
assert_eq!(m.len(), 3);
|
||||||
|
assert!(m.contains_key("color-scheme"));
|
||||||
|
assert!(m.contains_key("accent-color"));
|
||||||
|
assert!(m.contains_key("contrast"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_unknown_namespace_errs() {
|
||||||
|
let facts = theme_facts::facts_for("Nebula");
|
||||||
|
assert!(lookup(&facts, "org.gnome.desktop.interface", "color-scheme").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_unknown_key_errs() {
|
||||||
|
let facts = theme_facts::facts_for("Nebula");
|
||||||
|
assert!(lookup(&facts, APPEARANCE_NS, "no-such-key").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_color_scheme_ok() {
|
||||||
|
let facts = theme_facts::facts_for("Solarized Light");
|
||||||
|
assert!(lookup(&facts, APPEARANCE_NS, "color-scheme").is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//! Tabla de hechos del tema relevantes para el portal.
|
||||||
|
//!
|
||||||
|
//! El portal sólo necesita tres hechos de cada tema: si es oscuro, su
|
||||||
|
//! color de acento, y si es de alto contraste. La fuente de verdad de
|
||||||
|
//! la paleta completa es `nahual_theme::Theme` (crate `nahual-theme`);
|
||||||
|
//! esta tabla la **espeja deliberadamente** para que el daemon del
|
||||||
|
//! portal no tenga que enlazar GPUI (que `nahual-theme` arrastra por
|
||||||
|
//! sus tipos `Hsla`/`Background`).
|
||||||
|
//!
|
||||||
|
//! Si se agrega un preset nuevo a `nahual_theme::Theme::all()`, hay que
|
||||||
|
//! reflejarlo aquí. Un nombre desconocido cae a [`FALLBACK`] — el
|
||||||
|
//! portal degrada a "oscuro, sin acento marcado" en vez de romperse.
|
||||||
|
|
||||||
|
/// Hechos de un tema que el portal expone por `org.freedesktop.appearance`.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ThemeFacts {
|
||||||
|
/// `true` → el tema es oscuro (`color-scheme` = 1).
|
||||||
|
pub is_dark: bool,
|
||||||
|
/// `true` → alto contraste (`contrast` = 1).
|
||||||
|
pub high_contrast: bool,
|
||||||
|
/// Color de acento en HSL: `(matiz 0..360, saturación 0..1, luz 0..1)`.
|
||||||
|
/// Se guarda en HSL porque así está escrito en `nahual-theme` — la
|
||||||
|
/// conversión a RGB se hace al servir el valor.
|
||||||
|
pub accent_hsl: (f64, f64, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeFacts {
|
||||||
|
/// `color-scheme` de `org.freedesktop.appearance`: 0 = sin
|
||||||
|
/// preferencia, 1 = oscuro, 2 = claro. El escritorio siempre tiene
|
||||||
|
/// un tema activo, así que nunca devolvemos 0.
|
||||||
|
pub fn color_scheme(&self) -> u32 {
|
||||||
|
if self.is_dark {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `contrast` de `org.freedesktop.appearance`: 0 = normal,
|
||||||
|
/// 1 = contraste alto.
|
||||||
|
pub fn contrast(&self) -> u32 {
|
||||||
|
u32::from(self.high_contrast)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acento como RGB en 0..1, el formato `(ddd)` que pide el portal.
|
||||||
|
pub fn accent_rgb(&self) -> (f64, f64, f64) {
|
||||||
|
let (h, s, l) = self.accent_hsl;
|
||||||
|
hsl_to_rgb(h, s, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tema por defecto si el nombre persistido no se reconoce: oscuro, sin
|
||||||
|
/// acento marcado (gris neutro). Degradación segura ante un preset
|
||||||
|
/// futuro que esta tabla aún no conozca.
|
||||||
|
pub const FALLBACK: ThemeFacts = ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (0.0, 0.0, 0.5),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mapea el nombre persistido de un tema a sus hechos. Espeja
|
||||||
|
/// `nahual_theme::Theme::all()` (8 presets al 2026-05-21). Los números
|
||||||
|
/// de acento están copiados literalmente de `nahual-theme/src/lib.rs`.
|
||||||
|
pub fn facts_for(name: &str) -> ThemeFacts {
|
||||||
|
match name.trim() {
|
||||||
|
"Nebula" => ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (280.0, 0.65, 0.65),
|
||||||
|
},
|
||||||
|
"Aurora" => ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (150.0, 0.70, 0.55),
|
||||||
|
},
|
||||||
|
"Sunset" => ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (15.0, 0.78, 0.62),
|
||||||
|
},
|
||||||
|
"Flat Dark" => ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (210.0, 0.70, 0.55),
|
||||||
|
},
|
||||||
|
"Solarized Light" => ThemeFacts {
|
||||||
|
is_dark: false,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (205.0, 0.69, 0.42),
|
||||||
|
},
|
||||||
|
"High Contrast" => ThemeFacts {
|
||||||
|
is_dark: true,
|
||||||
|
high_contrast: true,
|
||||||
|
accent_hsl: (60.0, 1.00, 0.60),
|
||||||
|
},
|
||||||
|
"Print Color" => ThemeFacts {
|
||||||
|
is_dark: false,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (15.0, 0.70, 0.40),
|
||||||
|
},
|
||||||
|
"Print B&W" => ThemeFacts {
|
||||||
|
is_dark: false,
|
||||||
|
high_contrast: false,
|
||||||
|
accent_hsl: (0.0, 0.00, 0.20),
|
||||||
|
},
|
||||||
|
_ => FALLBACK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HSL → RGB. `h` en grados 0..360, `s` y `l` en 0..1. Devuelve RGB en
|
||||||
|
/// 0..1. Algoritmo estándar (croma + segmento del matiz).
|
||||||
|
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
|
||||||
|
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||||
|
let h_prime = h.rem_euclid(360.0) / 60.0;
|
||||||
|
let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
|
||||||
|
let (r1, g1, b1) = match h_prime as u32 {
|
||||||
|
0 => (c, x, 0.0),
|
||||||
|
1 => (x, c, 0.0),
|
||||||
|
2 => (0.0, c, x),
|
||||||
|
3 => (0.0, x, c),
|
||||||
|
4 => (x, 0.0, c),
|
||||||
|
_ => (c, 0.0, x),
|
||||||
|
};
|
||||||
|
let m = l - c / 2.0;
|
||||||
|
(r1 + m, g1 + m, b1 + m)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn approx(a: f64, b: f64) -> bool {
|
||||||
|
(a - b).abs() < 1e-6
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_eq(got: (f64, f64, f64), want: (f64, f64, f64)) -> bool {
|
||||||
|
approx(got.0, want.0) && approx(got.1, want.1) && approx(got.2, want.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hsl_primaries() {
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(0.0, 1.0, 0.5), (1.0, 0.0, 0.0)));
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(120.0, 1.0, 0.5), (0.0, 1.0, 0.0)));
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(240.0, 1.0, 0.5), (0.0, 0.0, 1.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hsl_grays() {
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 0.0), (0.0, 0.0, 0.0)));
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 1.0), (1.0, 1.0, 1.0)));
|
||||||
|
// Acento de "Print B&W": gris medio-oscuro.
|
||||||
|
assert!(rgb_eq(hsl_to_rgb(0.0, 0.0, 0.2), (0.2, 0.2, 0.2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_themes_map_color_scheme() {
|
||||||
|
assert_eq!(facts_for("Nebula").color_scheme(), 1);
|
||||||
|
assert_eq!(facts_for("Aurora").color_scheme(), 1);
|
||||||
|
assert_eq!(facts_for("Solarized Light").color_scheme(), 2);
|
||||||
|
assert_eq!(facts_for("Print Color").color_scheme(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn high_contrast_only_for_high_contrast_theme() {
|
||||||
|
assert!(facts_for("High Contrast").high_contrast);
|
||||||
|
assert_eq!(facts_for("High Contrast").contrast(), 1);
|
||||||
|
assert!(!facts_for("Nebula").high_contrast);
|
||||||
|
assert_eq!(facts_for("Nebula").contrast(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_theme_falls_back() {
|
||||||
|
let f = facts_for("NoSuchTheme");
|
||||||
|
assert_eq!(f, FALLBACK);
|
||||||
|
assert_eq!(f.color_scheme(), 1, "FALLBACK es oscuro");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accent_rgb_in_range() {
|
||||||
|
for name in [
|
||||||
|
"Nebula",
|
||||||
|
"Aurora",
|
||||||
|
"Sunset",
|
||||||
|
"Flat Dark",
|
||||||
|
"Solarized Light",
|
||||||
|
"High Contrast",
|
||||||
|
"Print Color",
|
||||||
|
"Print B&W",
|
||||||
|
] {
|
||||||
|
let (r, g, b) = facts_for(name).accent_rgb();
|
||||||
|
for ch in [r, g, b] {
|
||||||
|
assert!(
|
||||||
|
(0.0..=1.0).contains(&ch),
|
||||||
|
"{name}: canal fuera de rango: {ch}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada — el Cerebro del compositor: ventana GPUI que tesela el escritorio sobre mirada-brain y manda la geometría al Cuerpo (smithay) por mirada-link. Sin Cuerpo, corre en simulación."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mirada"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
|
||||||
|
mirada-link = { path = "../../modules/mirada/mirada-link" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,607 @@
|
|||||||
|
//! `mirada` — la ventana del Cerebro del compositor.
|
||||||
|
//!
|
||||||
|
//! Es el "Cerebro" de la arquitectura carmen hecho app GPUI: envuelve
|
||||||
|
//! [`mirada_brain::Desktop`] (toda la lógica de teselado y foco) y lo
|
||||||
|
//! pinta. La cadena completa:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! mirada-layout ─► mirada-protocol ─► mirada-brain ─► [esta ventana]
|
||||||
|
//! │
|
||||||
|
//! mirada-link ─► mirada-compositor (Cuerpo)
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Con un Cuerpo conectado (variable `MIRADA_SOCKET`) sondea sus
|
||||||
|
//! [`BodyEvent`]s y le devuelve [`BrainCommand`]s por el socket. Sin
|
||||||
|
//! Cuerpo arranca en **simulación**: las ventanas son sintéticas y el
|
||||||
|
//! teclado de esta ventana maneja el escritorio — útil para ver el
|
||||||
|
//! motor de teselado sin hardware.
|
||||||
|
//!
|
||||||
|
//! Teclas (simulación):
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! n / Shift+n abre ventana / monitor tab / espacio cicla layout
|
||||||
|
//! w cierra la enfocada t m g c r d s layout directo
|
||||||
|
//! f / Shift+f flota / pantalla completa h / l área maestra −/+
|
||||||
|
//! j / k foco siguiente/anterior , / . nmaster −/+
|
||||||
|
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
||||||
|
//! Enter promueve a maestra Ctrl+1..9 enviar a escritorio
|
||||||
|
//! o siguiente monitor ` / Shift+` scratchpad ver/guardar
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
||||||
|
//! `mirada-ctl` controla el escritorio desde la terminal — ambos pasan
|
||||||
|
//! por el mismo `Desktop::apply`.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
div, hsla, prelude::*, px, Context, FocusHandle, IntoElement, KeyDownEvent, MouseButton,
|
||||||
|
Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use mirada_brain::{
|
||||||
|
BodyEvent, BrainCommand, CtlConn, CtlReply, CtlRequest, CtlServer, Desktop, DesktopAction,
|
||||||
|
Keymap, KeymapWatch, LayoutMode, Rules, WindowId, WindowPlacement,
|
||||||
|
};
|
||||||
|
use mirada_link::BrainLink;
|
||||||
|
use nahual_launcher::launch_app;
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
|
/// Pantalla virtual del modo simulación — coincide con el lienzo.
|
||||||
|
const SCREEN_W: i32 = 1280;
|
||||||
|
const SCREEN_H: i32 = 720;
|
||||||
|
/// Periodo del sondeo del Cuerpo, en ms (~60 Hz).
|
||||||
|
const POLL_MS: u64 = 16;
|
||||||
|
|
||||||
|
/// Nombres de app ficticios para las ventanas de simulación.
|
||||||
|
const APPS: &[&str] = &[
|
||||||
|
"shuma", "fana", "revista", "cosmobiología", "matilda", "yachay", "barra",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// El Cerebro: el estado del escritorio + lo último colocado + el cable.
|
||||||
|
struct Mirada {
|
||||||
|
desktop: Desktop,
|
||||||
|
/// Geometría vigente — lo que se pinta. Es la última `Place` emitida.
|
||||||
|
placements: Vec<WindowPlacement>,
|
||||||
|
/// Contador de ids para las ventanas sintéticas.
|
||||||
|
next_id: WindowId,
|
||||||
|
/// Cable al Cuerpo; `None` en simulación.
|
||||||
|
link: Option<BrainLink>,
|
||||||
|
/// Última acción, para la barra de estado.
|
||||||
|
note: SharedString,
|
||||||
|
focus: FocusHandle,
|
||||||
|
focused_once: bool,
|
||||||
|
/// Ruta del keymap del usuario, para recargarlo en caliente.
|
||||||
|
keymap_path: Option<PathBuf>,
|
||||||
|
/// Vigía del keymap; `None` en simulación o si no hay archivo.
|
||||||
|
keymap_watch: Option<KeymapWatch>,
|
||||||
|
/// Socket del API de control externo (`mirada-ctl`).
|
||||||
|
ctl: Option<CtlServer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mirada {
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
// Keymap del usuario (~/.config/mirada/keymap.ron): define los
|
||||||
|
// atajos que el Cuerpo intercepta y nos devuelve como `Keybind`.
|
||||||
|
let keymap_path = Keymap::default_path();
|
||||||
|
let keymap = match &keymap_path {
|
||||||
|
Some(p) => Keymap::load_or_init(p),
|
||||||
|
None => Keymap::default(),
|
||||||
|
};
|
||||||
|
let link = connect_body();
|
||||||
|
// Vigilar el keymap sólo tiene sentido con un Cuerpo conectado;
|
||||||
|
// en simulación, mirada usa las teclas de su propia ventana.
|
||||||
|
let keymap_watch = if link.is_some() {
|
||||||
|
keymap_path.as_deref().and_then(|p| Keymap::watch(p).ok())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// API de control: mirada siempre posee el Desktop, así que
|
||||||
|
// siempre abre el socket de `mirada-ctl`.
|
||||||
|
let ctl = match CtlServer::bind(&mirada_brain::ctl::default_socket_path()) {
|
||||||
|
Ok(s) => Some(s),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("mirada · sin API de control: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reglas de ventana (~/.config/mirada/rules.ron): a qué
|
||||||
|
// escritorio va cada ventana, si flota.
|
||||||
|
let mut desktop = Desktop::with_keymap(keymap);
|
||||||
|
desktop.set_rules(load_user_rules());
|
||||||
|
|
||||||
|
let mut app = Self {
|
||||||
|
desktop,
|
||||||
|
placements: Vec::new(),
|
||||||
|
next_id: 1,
|
||||||
|
link,
|
||||||
|
note: SharedString::from("listo"),
|
||||||
|
focus: cx.focus_handle(),
|
||||||
|
focused_once: false,
|
||||||
|
keymap_path,
|
||||||
|
keymap_watch,
|
||||||
|
ctl,
|
||||||
|
};
|
||||||
|
if let Some(link) = app.link.as_mut() {
|
||||||
|
// Registra los atajos globales en el Cuerpo.
|
||||||
|
let _ = link.send(&app.desktop.grab_keys());
|
||||||
|
app.note = SharedString::from("Cuerpo conectado");
|
||||||
|
} else {
|
||||||
|
// Simulación: una pantalla virtual y tres ventanas de muestra.
|
||||||
|
app.feed(BodyEvent::OutputAdded { id: 0, width: SCREEN_W, height: SCREEN_H });
|
||||||
|
for _ in 0..3 {
|
||||||
|
app.open_window();
|
||||||
|
}
|
||||||
|
app.note = SharedString::from("simulación — sin Cuerpo");
|
||||||
|
}
|
||||||
|
// El sondeo corre siempre: drena el Cuerpo (si lo hay), vigila el
|
||||||
|
// keymap y atiende `mirada-ctl`.
|
||||||
|
app.start_poll(cx);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bucle de fondo: drena los eventos del Cuerpo y los procesa.
|
||||||
|
fn start_poll(&self, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn(async move |this, cx| loop {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(POLL_MS))
|
||||||
|
.await;
|
||||||
|
let alive = this.update(cx, |app, cx| {
|
||||||
|
let events: Vec<BodyEvent> = match app.link.as_ref() {
|
||||||
|
Some(link) => link.drain(),
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
let had_events = !events.is_empty();
|
||||||
|
let keymap_changed = app.keymap_watch.as_ref().is_some_and(|w| w.changed());
|
||||||
|
if keymap_changed {
|
||||||
|
app.reload_keymap();
|
||||||
|
}
|
||||||
|
let ctl_served = app.poll_ctl();
|
||||||
|
for ev in events {
|
||||||
|
app.feed(ev);
|
||||||
|
}
|
||||||
|
if had_events || keymap_changed || ctl_served {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if alive.is_err() {
|
||||||
|
break; // ventana cerrada
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abre una ventana sintética (sólo tiene sentido en simulación).
|
||||||
|
fn open_window(&mut self) {
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
let app = APPS[(id as usize) % APPS.len()];
|
||||||
|
self.feed(BodyEvent::WindowOpened {
|
||||||
|
id,
|
||||||
|
app_id: format!("org.brahman.{app}"),
|
||||||
|
title: format!("{app} · ventana {id}"),
|
||||||
|
});
|
||||||
|
self.note = SharedString::from(format!("abierta ventana {id}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inyecta un evento del Cuerpo en el `Desktop` y despacha la salida.
|
||||||
|
fn feed(&mut self, event: BodyEvent) {
|
||||||
|
let cmds = self.desktop.on_event(event);
|
||||||
|
self.dispatch(cmds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aplica una acción de escritorio (desde una tecla de esta ventana).
|
||||||
|
fn act(&mut self, action: DesktopAction) {
|
||||||
|
let cmds = self.desktop.apply(action);
|
||||||
|
self.dispatch(cmds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recarga el keymap del disco y re-registra los atajos en el Cuerpo.
|
||||||
|
fn reload_keymap(&mut self) {
|
||||||
|
let Some(path) = self.keymap_path.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match Keymap::load(&path) {
|
||||||
|
Ok(km) => {
|
||||||
|
let cmd = self.desktop.set_keymap(km);
|
||||||
|
self.dispatch(vec![cmd]);
|
||||||
|
self.note = SharedString::from("keymap recargado");
|
||||||
|
}
|
||||||
|
Err(e) => self.note = SharedString::from(format!("keymap inválido: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atiende las peticiones pendientes del API de control. Devuelve
|
||||||
|
/// `true` si sirvió alguna (para repintar).
|
||||||
|
fn poll_ctl(&mut self) -> bool {
|
||||||
|
let conns: Vec<CtlConn> = match &self.ctl {
|
||||||
|
Some(ctl) => std::iter::from_fn(|| ctl.poll()).collect(),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
let mut served = false;
|
||||||
|
for mut conn in conns {
|
||||||
|
let reply = match conn.read_request() {
|
||||||
|
Ok(Some(req)) => {
|
||||||
|
served = true;
|
||||||
|
self.serve_ctl(req)
|
||||||
|
}
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(e) => CtlReply::Error(format!("{e}")),
|
||||||
|
};
|
||||||
|
let _ = conn.reply(&reply);
|
||||||
|
}
|
||||||
|
served
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resuelve una petición de control: la acción pasa por el mismo
|
||||||
|
/// `apply` que el teclado; la consulta lee el `Desktop`.
|
||||||
|
fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
|
||||||
|
match req {
|
||||||
|
CtlRequest::Do(action) => {
|
||||||
|
self.act(action);
|
||||||
|
CtlReply::Ok
|
||||||
|
}
|
||||||
|
CtlRequest::ListWindows => CtlReply::Windows(self.desktop.window_lines()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reparte los comandos del Cerebro: actualiza lo pintado y, o bien
|
||||||
|
/// los manda al Cuerpo, o bien —en simulación— cierra las ventanas
|
||||||
|
/// por su cuenta (no hay nadie que devuelva el `WindowClosed`).
|
||||||
|
fn dispatch(&mut self, cmds: Vec<BrainCommand>) {
|
||||||
|
for cmd in &cmds {
|
||||||
|
if let BrainCommand::Place(p) = cmd {
|
||||||
|
self.placements = p.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match self.link.as_mut() {
|
||||||
|
Some(link) => {
|
||||||
|
for cmd in &cmds {
|
||||||
|
let _ = link.send(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
for cmd in cmds {
|
||||||
|
match cmd {
|
||||||
|
BrainCommand::Close(id) | BrainCommand::Kill(id) => {
|
||||||
|
self.feed(BodyEvent::WindowClosed { id });
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traduce una tecla de la ventana a una acción de escritorio.
|
||||||
|
fn handle_key(&mut self, event: &KeyDownEvent, _w: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let ks = &event.keystroke;
|
||||||
|
let ctrl = ks.modifiers.control;
|
||||||
|
let shift = ks.modifiers.shift;
|
||||||
|
let connected = self.link.is_some();
|
||||||
|
|
||||||
|
match ks.key.as_str() {
|
||||||
|
"n" if shift && !connected => {
|
||||||
|
// Simulación: añade un monitor más, en fila a la derecha.
|
||||||
|
let id = self.desktop.outputs().len() as u32;
|
||||||
|
self.feed(BodyEvent::OutputAdded { id, width: SCREEN_W, height: SCREEN_H });
|
||||||
|
}
|
||||||
|
"n" if !connected => self.open_window(),
|
||||||
|
"w" => self.act(DesktopAction::CloseFocused),
|
||||||
|
"f" if shift => self.act(DesktopAction::ToggleFullscreen),
|
||||||
|
"f" => self.act(DesktopAction::ToggleFloat),
|
||||||
|
"j" if shift => self.act(DesktopAction::MoveForward),
|
||||||
|
"k" if shift => self.act(DesktopAction::MoveBackward),
|
||||||
|
"j" => self.act(DesktopAction::FocusNext),
|
||||||
|
"k" => self.act(DesktopAction::FocusPrev),
|
||||||
|
"tab" | "space" => self.act(DesktopAction::CycleLayout),
|
||||||
|
"t" => self.act(DesktopAction::SetLayout(LayoutMode::MasterStack)),
|
||||||
|
"m" => self.act(DesktopAction::SetLayout(LayoutMode::Monocle)),
|
||||||
|
"g" => self.act(DesktopAction::SetLayout(LayoutMode::Grid)),
|
||||||
|
"c" => self.act(DesktopAction::SetLayout(LayoutMode::Columns)),
|
||||||
|
"r" => self.act(DesktopAction::SetLayout(LayoutMode::Rows)),
|
||||||
|
"d" => self.act(DesktopAction::SetLayout(LayoutMode::CenteredMaster)),
|
||||||
|
"s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)),
|
||||||
|
"h" => self.act(DesktopAction::ShrinkMaster),
|
||||||
|
"l" => self.act(DesktopAction::GrowMaster),
|
||||||
|
"o" => self.act(DesktopAction::FocusOutputNext),
|
||||||
|
"`" if shift => self.act(DesktopAction::SendToScratchpad),
|
||||||
|
"`" => self.act(DesktopAction::ToggleScratchpad),
|
||||||
|
"enter" => self.act(DesktopAction::PromoteToMaster),
|
||||||
|
"," => self.act(DesktopAction::IncMaster),
|
||||||
|
"." => self.act(DesktopAction::DecMaster),
|
||||||
|
d if d.len() == 1 && d.as_bytes()[0].is_ascii_digit() && d != "0" => {
|
||||||
|
let n = (d.as_bytes()[0] - b'1') as usize;
|
||||||
|
if ctrl {
|
||||||
|
self.act(DesktopAction::SendToWorkspace(n));
|
||||||
|
} else {
|
||||||
|
self.act(DesktopAction::SwitchWorkspace(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Conecta con el Cuerpo si `MIRADA_SOCKET` apunta a un socket vivo.
|
||||||
|
fn connect_body() -> Option<BrainLink> {
|
||||||
|
let path = std::env::var("MIRADA_SOCKET").ok()?;
|
||||||
|
BrainLink::connect(&path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
|
||||||
|
fn load_user_rules() -> Rules {
|
||||||
|
match Rules::default_path() {
|
||||||
|
Some(p) => Rules::load_or_default(&p),
|
||||||
|
None => Rules::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nombre legible de un modo de teselado.
|
||||||
|
fn mode_name(m: LayoutMode) -> &'static str {
|
||||||
|
match m {
|
||||||
|
LayoutMode::MasterStack => "maestro + pila",
|
||||||
|
LayoutMode::Monocle => "monóculo",
|
||||||
|
LayoutMode::Grid => "rejilla",
|
||||||
|
LayoutMode::Columns => "columnas",
|
||||||
|
LayoutMode::Rows => "filas",
|
||||||
|
LayoutMode::CenteredMaster => "maestro centrado",
|
||||||
|
LayoutMode::Spiral => "espiral",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Mirada {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
// El lienzo necesita el foco del teclado desde el primer frame.
|
||||||
|
if !self.focused_once {
|
||||||
|
window.focus(&self.focus);
|
||||||
|
self.focused_once = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let win_bg = hsla(220.0 / 360.0, 0.16, 0.13, 1.0);
|
||||||
|
let bar_bg = hsla(220.0 / 360.0, 0.20, 0.09, 1.0);
|
||||||
|
let canvas_bg = hsla(220.0 / 360.0, 0.24, 0.05, 1.0);
|
||||||
|
// Texto legible sobre un fondo de acento.
|
||||||
|
let on_accent = hsla(220.0 / 360.0, 0.24, 0.06, 1.0);
|
||||||
|
|
||||||
|
let active = self.desktop.active_index();
|
||||||
|
let mode = self.desktop.active_workspace().params().mode;
|
||||||
|
let loads = self.desktop.workspace_loads();
|
||||||
|
let focused = self.desktop.focused_window();
|
||||||
|
|
||||||
|
// --- Barra superior: identidad + escritorios + modo ----------
|
||||||
|
let pips = loads.iter().enumerate().map(|(i, &load)| {
|
||||||
|
let is_active = i == active;
|
||||||
|
let fg = if is_active {
|
||||||
|
on_accent
|
||||||
|
} else if load > 0 {
|
||||||
|
theme.fg_text
|
||||||
|
} else {
|
||||||
|
theme.fg_disabled
|
||||||
|
};
|
||||||
|
div()
|
||||||
|
.w(px(24.))
|
||||||
|
.h(px(22.))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.rounded(px(4.))
|
||||||
|
.cursor_pointer()
|
||||||
|
.when(is_active, |d| d.bg(theme.accent))
|
||||||
|
.when(!is_active && load > 0, |d| d.bg(theme.bg_row_hover))
|
||||||
|
.text_color(fg)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |app, _, _, cx| {
|
||||||
|
app.act(DesktopAction::SwitchWorkspace(i));
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(SharedString::from(format!("{}", i + 1)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let focus_label = match focused.and_then(|id| self.desktop.window_info(id)) {
|
||||||
|
Some(info) => info.title.clone(),
|
||||||
|
None => "—".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let bar = div()
|
||||||
|
.h(px(44.))
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(12.))
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(bar_bg)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(div().text_color(theme.accent).child("mirada"))
|
||||||
|
.child(div().text_color(theme.fg_disabled).child("·"))
|
||||||
|
.child(div().flex().flex_row().gap(px(4.)).children(pips))
|
||||||
|
.child(div().text_color(theme.fg_disabled).child("·"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("layout: {}", mode_name(mode)))),
|
||||||
|
)
|
||||||
|
.child(div().flex_1())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("foco: {focus_label}"))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Lienzo: el escritorio teselado, a escala ----------------
|
||||||
|
// El lienzo es de tamaño fijo; el contenido vive en el espacio
|
||||||
|
// global de las salidas. `scale` encaja ese espacio en el lienzo
|
||||||
|
// — con una sola salida, escala 1:1.
|
||||||
|
let outs = self.desktop.outputs();
|
||||||
|
let (bb_w, bb_h) = if outs.is_empty() {
|
||||||
|
(SCREEN_W as f32, SCREEN_H as f32)
|
||||||
|
} else {
|
||||||
|
let w = outs.iter().map(|o| o.rect.x + o.rect.w).max().unwrap_or(SCREEN_W);
|
||||||
|
let h = outs.iter().map(|o| o.rect.y + o.rect.h).max().unwrap_or(SCREEN_H);
|
||||||
|
(w as f32, h as f32)
|
||||||
|
};
|
||||||
|
let scale = (SCREEN_W as f32 / bb_w)
|
||||||
|
.min(SCREEN_H as f32 / bb_h)
|
||||||
|
.min(1.0);
|
||||||
|
|
||||||
|
let mut canvas = div()
|
||||||
|
.relative()
|
||||||
|
.w(px(SCREEN_W as f32))
|
||||||
|
.h(px(SCREEN_H as f32))
|
||||||
|
.bg(canvas_bg)
|
||||||
|
.overflow_hidden();
|
||||||
|
|
||||||
|
// Un marco por salida, con su número y el escritorio que muestra.
|
||||||
|
for (i, o) in outs.iter().enumerate() {
|
||||||
|
let is_focused_out = i == self.desktop.focused_output();
|
||||||
|
canvas = canvas.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(o.rect.x as f32 * scale))
|
||||||
|
.top(px(o.rect.y as f32 * scale))
|
||||||
|
.w(px(o.rect.w as f32 * scale))
|
||||||
|
.h(px(o.rect.h as f32 * scale))
|
||||||
|
.border_1()
|
||||||
|
.border_color(if is_focused_out {
|
||||||
|
theme.accent
|
||||||
|
} else {
|
||||||
|
theme.border
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top(px(2.))
|
||||||
|
.left(px(4.))
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"salida {} · escritorio {}",
|
||||||
|
i + 1,
|
||||||
|
o.workspace + 1
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let visible = self.placements.iter().filter(|p| p.visible).count();
|
||||||
|
if visible == 0 {
|
||||||
|
canvas = canvas.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child("escritorio vacío — pulsa n para abrir una ventana"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for p in self.placements.iter().filter(|p| p.visible) {
|
||||||
|
let info = self.desktop.window_info(p.id);
|
||||||
|
let title = info
|
||||||
|
.map(|i| i.title.clone())
|
||||||
|
.unwrap_or_else(|| format!("ventana {}", p.id));
|
||||||
|
let app_id = info.map(|i| i.app_id.clone()).unwrap_or_default();
|
||||||
|
let border = if p.focused { theme.accent } else { theme.border };
|
||||||
|
let tb_bg = if p.focused { theme.accent } else { theme.bg_row_hover };
|
||||||
|
let tb_fg = if p.focused { on_accent } else { theme.fg_muted };
|
||||||
|
let pid = p.id;
|
||||||
|
let kind_label = if p.fullscreen {
|
||||||
|
"· pantalla completa ·"
|
||||||
|
} else if p.floating {
|
||||||
|
"· ventana flotante ·"
|
||||||
|
} else {
|
||||||
|
"· superficie del Cuerpo ·"
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas = canvas.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(p.rect.x as f32 * scale))
|
||||||
|
.top(px(p.rect.y as f32 * scale))
|
||||||
|
.w(px(p.rect.w as f32 * scale))
|
||||||
|
.h(px(p.rect.h as f32 * scale))
|
||||||
|
.border_2()
|
||||||
|
.border_color(border)
|
||||||
|
.bg(win_bg)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.overflow_hidden()
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |app, _, _, cx| {
|
||||||
|
app.act(DesktopAction::FocusWindow(pid));
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(
|
||||||
|
// Barra de título de la ventana.
|
||||||
|
div()
|
||||||
|
.h(px(22.))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.px(px(8.))
|
||||||
|
.bg(tb_bg)
|
||||||
|
.text_color(tb_fg)
|
||||||
|
.child(SharedString::from(title)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
// Interior: en el compositor real lo compone el
|
||||||
|
// Cuerpo (zero-copy); aquí es un marcador.
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap(px(4.))
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child(SharedString::from(app_id))
|
||||||
|
.child(kind_label),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Composición ---------------------------------------------
|
||||||
|
div()
|
||||||
|
.track_focus(&self.focus)
|
||||||
|
.key_context("Mirada")
|
||||||
|
.on_key_down(cx.listener(Self::handle_key))
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(bar)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(canvas),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
// Pie: el estado.
|
||||||
|
div()
|
||||||
|
.h(px(26.))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(bar_bg)
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child(self.note.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch_app("brahman · mirada", (SCREEN_W as f32, (SCREEN_H + 70) as f32), Mirada::new);
|
||||||
|
}
|
||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yahweh-database-explorer"
|
name = "nahual-database-explorer"
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
@@ -7,7 +7,7 @@ description = "Explorer de SQLite — composición TreeView + SqliteProvider con
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
yahweh-core = { workspace = true }
|
nahual-core = { workspace = true }
|
||||||
yahweh-theme = { workspace = true }
|
nahual-theme = { workspace = true }
|
||||||
yahweh-widget-tree = { workspace = true }
|
nahual-widget-tree = { workspace = true }
|
||||||
yahweh-provider-sqlite = { workspace = true }
|
nahual-provider-sqlite = { workspace = true }
|
||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
//! `yahweh_database_explorer` — explorer de SQLite.
|
//! `nahual_database_explorer` — explorer de SQLite.
|
||||||
//!
|
//!
|
||||||
//! Mismo patrón que `yahweh_file_explorer` pero con `SqliteProvider`. La
|
//! Mismo patrón que `nahual_file_explorer` pero con `SqliteProvider`. La
|
||||||
//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
|
//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
|
||||||
//! origen de los datos: filas de una tabla `items(id, parent_id, name,
|
//! origen de los datos: filas de una tabla `items(id, parent_id, name,
|
||||||
//! display_type, content)` en lugar del filesystem.
|
//! display_type, content)` en lugar del filesystem.
|
||||||
@@ -13,10 +13,10 @@ use gpui::{
|
|||||||
px,
|
px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use yahweh_core::{DataProvider, DisplayType, EntityNode};
|
use nahual_core::{DataProvider, DisplayType, EntityNode};
|
||||||
use yahweh_provider_sqlite::SqliteDataProvider;
|
use nahual_provider_sqlite::SqliteDataProvider;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
|
use nahual_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
|
#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yahweh-file-explorer"
|
name = "nahual-file-explorer"
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
@@ -7,8 +7,8 @@ description = "Explorer de filesystem — composición TreeView + FsProvider con
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
yahweh-core = { workspace = true }
|
nahual-core = { workspace = true }
|
||||||
yahweh-theme = { workspace = true }
|
nahual-theme = { workspace = true }
|
||||||
yahweh-widget-tree = { workspace = true }
|
nahual-widget-tree = { workspace = true }
|
||||||
yahweh-widget-text-input = { workspace = true }
|
nahual-widget-text-input = { workspace = true }
|
||||||
yahweh-provider-fs = { workspace = true }
|
nahual-provider-fs = { workspace = true }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `yahweh_file_explorer` — explorer de filesystem con menú contextual.
|
//! `nahual_file_explorer` — explorer de filesystem con menú contextual.
|
||||||
//!
|
//!
|
||||||
//! Composición canónica del patrón "explorer = TreeView + provider":
|
//! Composición canónica del patrón "explorer = TreeView + provider":
|
||||||
//!
|
//!
|
||||||
@@ -42,11 +42,11 @@ use gpui::{
|
|||||||
PromptLevel, Render, SharedString, Window, div, prelude::*, px,
|
PromptLevel, Render, SharedString, Window, div, prelude::*, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use yahweh_core::{DataProvider, DisplayType, EntityNode};
|
use nahual_core::{DataProvider, DisplayType, EntityNode};
|
||||||
use yahweh_provider_fs::FileDataProvider;
|
use nahual_provider_fs::FileDataProvider;
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
use yahweh_widget_text_input::{TextInput, TextInputEvent};
|
use nahual_widget_text_input::{TextInput, TextInputEvent};
|
||||||
use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
|
use nahual_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yahweh-image-viewer"
|
name = "nahual-image-viewer"
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
@@ -7,5 +7,5 @@ description = "Visor de imágenes. Suscribe al AppBus y renderea con gpui::img(p
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
yahweh-bus = { workspace = true }
|
nahual-bus = { workspace = true }
|
||||||
yahweh-theme = { workspace = true }
|
nahual-theme = { workspace = true }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! `yahweh_image_viewer` — visor de imágenes.
|
//! `nahual_image_viewer` — visor de imágenes.
|
||||||
//!
|
//!
|
||||||
//! Suscribe al `AppBus` y, en cada `EntitySelected` cuyo provider sea
|
//! Suscribe al `AppBus` y, en cada `EntitySelected` cuyo provider sea
|
||||||
//! `local_fs` y la extensión sugiera imagen (jpg, png, webp, gif), pasa el
|
//! `local_fs` y la extensión sugiera imagen (jpg, png, webp, gif), pasa el
|
||||||
@@ -17,8 +17,8 @@ use gpui::{
|
|||||||
Context, Entity, IntoElement, Render, SharedString, Window, div, img, prelude::*, px,
|
Context, Entity, IntoElement, Render, SharedString, Window, div, img, prelude::*, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use yahweh_bus::{AppBus, AppEvent};
|
use nahual_bus::{AppBus, AppEvent};
|
||||||
use yahweh_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
const FS_PROVIDER: &str = "local_fs";
|
const FS_PROVIDER: &str = "local_fs";
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "nahual-shell"
|
||||||
|
version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
description = "Bootstrap GPUI + LayoutHost de Yahweh."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nahual-core = { workspace = true }
|
||||||
|
nahual-theme = { workspace = true }
|
||||||
|
nahual-provider-fs = { workspace = true }
|
||||||
|
nahual-provider-sqlite = { workspace = true }
|
||||||
|
nahual-widget-tree = { workspace = true }
|
||||||
|
nahual-widget-container-core = { workspace = true }
|
||||||
|
nahual-widget-splitter = { workspace = true }
|
||||||
|
nahual-widget-tabs = { workspace = true }
|
||||||
|
nahual-widget-tiled = { workspace = true }
|
||||||
|
nahual-bus = { workspace = true }
|
||||||
|
nahual-file-explorer = { workspace = true }
|
||||||
|
nahual-database-explorer = { workspace = true }
|
||||||
|
nahual-text-viewer = { workspace = true }
|
||||||
|
nahual-image-viewer = { workspace = true }
|
||||||
|
gpui = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
notify = { workspace = true }
|
||||||
|
|
||||||
|
# Brahman protocol — sidecar thread que se presenta al Init.
|
||||||
|
brahman-card = { path = "../../protocol/brahman-card" }
|
||||||
|
brahman-sidecar = { path = "../../protocol/brahman-sidecar" }
|
||||||
|
ulid = { workspace = true }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "nahual"
|
||||||
|
path = "src/main.rs"
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
//! Card de yahweh-shell + spawn del sidecar brahman compartido.
|
//! Card de nahual-shell + spawn del sidecar brahman compartido.
|
||||||
//!
|
//!
|
||||||
//! La lógica de thread + tokio + ping-loop vive en `brahman-sidecar`;
|
//! La lógica de thread + tokio + ping-loop vive en `brahman-sidecar`;
|
||||||
//! aquí sólo declaramos la identidad de yahweh como módulo Widget.
|
//! aquí sólo declaramos la identidad de nahual como módulo Widget.
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ use brahman_card::{
|
|||||||
};
|
};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
/// Spawn del sidecar con la Card de yahweh.
|
/// Spawn del sidecar con la Card de nahual.
|
||||||
pub fn spawn() {
|
pub fn spawn() {
|
||||||
brahman_sidecar::spawn(build_card());
|
brahman_sidecar::spawn(build_card());
|
||||||
}
|
}
|
||||||
+1
-1
@@ -26,7 +26,7 @@ use std::time::Duration;
|
|||||||
use gpui::{App, AsyncApp, Entity};
|
use gpui::{App, AsyncApp, Entity};
|
||||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
|
||||||
use yahweh_core::LayerConfig;
|
use nahual_core::LayerConfig;
|
||||||
|
|
||||||
use crate::layout_model::LayoutModel;
|
use crate::layout_model::LayoutModel;
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user