From bf782ebef1b560208994c5868a1f222678cd82f1 Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 4 Jun 2026 12:18:02 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20khipu=20standalone=20=E2=80=94=20notas?= =?UTF-8?q?=20P2P=20soberanas=20content-addressed,=20local-first=20(front-?= =?UTF-8?q?door,=20git-dep=20al=20monorepo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Front-door limpio: solo crates del dominio; Llimphi y lo fundacional por git-dep del monorepo gioser.git. cargo check pasa (5 crates, 0 errores). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + 00_unanchay/khipu/LEEME.md | 78 + 00_unanchay/khipu/README.md | 30 + 00_unanchay/khipu/README.qu.md | 32 + 00_unanchay/khipu/khipu-app/Cargo.toml | 41 + 00_unanchay/khipu/khipu-app/LEEME.md | 17 + 00_unanchay/khipu/khipu-app/README.md | 17 + .../khipu/khipu-app/examples/demo_cli.rs | 159 + 00_unanchay/khipu/khipu-app/src/main.rs | 1928 +++++ 00_unanchay/khipu/khipu-app/src/map.rs | 655 ++ 00_unanchay/khipu/khipu-app/src/net.rs | 226 + 00_unanchay/khipu/khipu-app/src/panels.rs | 728 ++ 00_unanchay/khipu/khipu-brahman/Cargo.toml | 22 + 00_unanchay/khipu/khipu-brahman/src/lib.rs | 275 + .../khipu-brahman/tests/p2p_roundtrip.rs | 193 + 00_unanchay/khipu/khipu-core/Cargo.toml | 11 + 00_unanchay/khipu/khipu-core/LEEME.md | 21 + 00_unanchay/khipu/khipu-core/README.md | 21 + 00_unanchay/khipu/khipu-core/src/lib.rs | 23 + 00_unanchay/khipu/khipu-core/src/links.rs | 93 + 00_unanchay/khipu/khipu-core/src/note.rs | 108 + 00_unanchay/khipu/khipu-core/src/store.rs | 348 + 00_unanchay/khipu/khipu-gravity/Cargo.toml | 15 + 00_unanchay/khipu/khipu-gravity/LEEME.md | 20 + 00_unanchay/khipu/khipu-gravity/README.md | 20 + 00_unanchay/khipu/khipu-gravity/src/lib.rs | 479 ++ 00_unanchay/khipu/khipu-share/Cargo.toml | 20 + .../khipu/khipu-share/src/discovery.rs | 180 + 00_unanchay/khipu/khipu-share/src/identity.rs | 125 + 00_unanchay/khipu/khipu-share/src/lib.rs | 349 + 00_unanchay/khipu/khipu-share/src/net.rs | 155 + .../khipu/khipu-share/tests/lan_roundtrip.rs | 75 + Cargo.lock | 6692 +++++++++++++++++ Cargo.toml | 439 ++ LICENSE | 21 + README.md | 11 + 36 files changed, 13630 insertions(+) create mode 100644 .gitignore create mode 100644 00_unanchay/khipu/LEEME.md create mode 100644 00_unanchay/khipu/README.md create mode 100644 00_unanchay/khipu/README.qu.md create mode 100644 00_unanchay/khipu/khipu-app/Cargo.toml create mode 100644 00_unanchay/khipu/khipu-app/LEEME.md create mode 100644 00_unanchay/khipu/khipu-app/README.md create mode 100644 00_unanchay/khipu/khipu-app/examples/demo_cli.rs create mode 100644 00_unanchay/khipu/khipu-app/src/main.rs create mode 100644 00_unanchay/khipu/khipu-app/src/map.rs create mode 100644 00_unanchay/khipu/khipu-app/src/net.rs create mode 100644 00_unanchay/khipu/khipu-app/src/panels.rs create mode 100644 00_unanchay/khipu/khipu-brahman/Cargo.toml create mode 100644 00_unanchay/khipu/khipu-brahman/src/lib.rs create mode 100644 00_unanchay/khipu/khipu-brahman/tests/p2p_roundtrip.rs create mode 100644 00_unanchay/khipu/khipu-core/Cargo.toml create mode 100644 00_unanchay/khipu/khipu-core/LEEME.md create mode 100644 00_unanchay/khipu/khipu-core/README.md create mode 100644 00_unanchay/khipu/khipu-core/src/lib.rs create mode 100644 00_unanchay/khipu/khipu-core/src/links.rs create mode 100644 00_unanchay/khipu/khipu-core/src/note.rs create mode 100644 00_unanchay/khipu/khipu-core/src/store.rs create mode 100644 00_unanchay/khipu/khipu-gravity/Cargo.toml create mode 100644 00_unanchay/khipu/khipu-gravity/LEEME.md create mode 100644 00_unanchay/khipu/khipu-gravity/README.md create mode 100644 00_unanchay/khipu/khipu-gravity/src/lib.rs create mode 100644 00_unanchay/khipu/khipu-share/Cargo.toml create mode 100644 00_unanchay/khipu/khipu-share/src/discovery.rs create mode 100644 00_unanchay/khipu/khipu-share/src/identity.rs create mode 100644 00_unanchay/khipu/khipu-share/src/lib.rs create mode 100644 00_unanchay/khipu/khipu-share/src/net.rs create mode 100644 00_unanchay/khipu/khipu-share/tests/lan_roundtrip.rs create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7141ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +*.pdb diff --git a/00_unanchay/khipu/LEEME.md b/00_unanchay/khipu/LEEME.md new file mode 100644 index 0000000..ebc6d31 --- /dev/null +++ b/00_unanchay/khipu/LEEME.md @@ -0,0 +1,78 @@ +# khipu + +> `khipu` (quechua: nudos de cuerdas para registrar memoria). Notas con gravedad temporal. + +Captura de notas rápidas donde el olvido es parte del modelo: cada nota tiene una masa que decae con el tiempo y se refuerza con cada acceso. Lo recurrente queda visible; lo que no se vuelve a tocar se va difuminando hasta caer del horizonte. + +## Instalación + +```sh +cargo run --release -p khipu-app +``` + +## Compatibilidad + +- **Linux / macOS / Windows** — UI Llimphi (Wayland/X11/Win32 vía `winit`). +- Persistencia local en `$XDG_DATA_HOME/khipu/`. + +## Crates + +| Crate | Rol | +|---|---| +| [`khipu-core`](khipu-core/README.md) | Modelo de nota + store; sin UI. | +| [`khipu-gravity`](khipu-gravity/README.md) | Algoritmo de masa/decay; refuerzo por acceso. | +| `khipu-share` | Sobres de notas firmados (Ed25519) y direccionados por contenido (BLAKE3) sobre agora; transporte TCP/LAN + descubrimiento UDP + identidad cifrada. | +| `khipu-brahman` | Transporte de sobres sobre libp2p (BrahmanNet): stream cifrado + descubrimiento por DHT. | +| [`khipu-app`](khipu-app/README.md) | UI Llimphi sobre el core. | + +## Gravedad semántica (embeddings) + +El canvas de la derecha agrupa las notas por afinidad. Los vectores salen del `verbo-daemon` si está corriendo; si no, de un hash-trigram local. + +```sh +# Embeddings reales (clústeres y vecinos semánticos de verdad): +cargo run -p rimay-verbo-daemon-bin -- --provider fastembed # escucha en $XDG_RUNTIME_DIR/verbo.sock +cargo run --release -p khipu-app # lo detecta solo al arrancar +``` + +Sin daemon, khipu cae al embebedor trigram de 16d — determinista y offline, idéntico al comportamiento histórico. El cálculo nunca bloquea la UI: viaja a un worker y reentra al bucle cuando termina. Si el espacio vectorial cambia entre dos arranques (arrancó/cayó el daemon, otro modelo), los vectores se recalculan automáticamente. + +## Compartir (agora) + +`exportar` sella en `compartido.khipu` un sobre firmado Ed25519 con la identidad del cuaderno y direccionado por su hash BLAKE3 de contenido. + +La identidad vive **cifrada** (Argon2id + ChaCha20-Poly1305, vía `agora-keystore`) en `/keys/` — la semilla privada nunca queda en claro en disco. Al primer intento de compartir, khipu pide una passphrase: la primera vez la crea, después la usa para descifrar. `KHIPU_PASSPHRASE` en el entorno desbloquea sin prompt (headless). Una `identidad.seed` en claro de versiones viejas se migra al keystore (y se borra el claro) automáticamente. Comparte **lo que el buscador esté filtrando** (vacío = todo el cuaderno), así que escribir en la búsqueda y exportar manda sólo ese subconjunto. `importar` verifica firma + hash y, si cuadra, ingiere las notas, marcándolas con una etiqueta de procedencia `de:` (visible en el editor como «✎ de: …»). + +Lo que viaja es el **contenido** (título, cuerpo, etiquetas), nunca la física temporal: al importar, cada nota nace fresca (masa plena, acceso = ahora) — su gravedad arranca en el cuaderno que la recibe. Los wiki-links `[[Título]]` se rearman solos porque khipu resuelve enlaces por título. Reimportar el mismo sobre no duplica (se omiten títulos ya presentes). Un sobre alterado o con firma ajena se rechaza entero, sin autoridad central. + +Para compartir en vivo sin copiar archivos: `publicar` levanta un servidor TCP que sirve el cuaderno (puerto `KHIPU_BIND`, default `127.0.0.1:7700`) **y anuncia una baliza UDP** para que lo descubran en la LAN. `recibir` abre un panel con un **campo de dirección** (`host:puerto`, prellenado y editable) y, debajo, los **pares descubiertos en la LAN** (nombre · autor · dirección): click en uno para jalarle el cuaderno, o escribí una dirección y «jalar». El transporte es `std::net` puro y **no necesita ser confiable** — el receptor verifica firma + hash antes de ingerir; la baliza sólo dice *dónde*, no *qué*. + +**WAN / libp2p**: el campo de dirección de `recibir` acepta dos formas, autodetectadas: `host:puerto` (TCP directo) o una **multiaddr libp2p** —directa `/ip4/…/p2p/` o de circuito `/ip4/…/p2p//p2p-circuit/p2p/`. Al `publicar`, khipu sirve por libp2p (stream cifrado Noise sobre `BrahmanNet`, protocolo `/khipu/sobre/1.0.0`, vía `khipu-brahman`) y muestra tu dirección de marcado. + +**NAT traversal**: `BrahmanNet` ahora trae **Circuit Relay v2 + DCUtR** (`card-net`). Un nodo alcanzable hace de relay; uno detrás de NAT reserva un circuito ahí y queda accesible vía la dirección de circuito. Configurá `KHIPU_RELAY=/ip4/…/tcp/…/p2p/` antes de `publicar` y khipu reserva el circuito y muestra la dirección para compartir. Las direcciones externas no se confían a ciegas: **AutoNAT** las confirma pidiendo dial-backs a otros peers, y sólo las confirmadas se anuncian (y entran en las reservas de relay). En la malla Brahman AutoNAT corre con `only_global_ips=false` para confirmar también en LAN/loopback. + +**Descubrimiento por DHT**: con `KHIPU_BOOTSTRAP=/ip4/…/p2p/` (un nodo de la malla), khipu se une a la DHT Kademlia al arrancar; `publicar` se anuncia bajo la clave khipu y `recibir` lista —además de los pares LAN— los pares hallados por DHT (filas `DHT · …`), que se jalan por peer-id. Así dos khipu se encuentran sin compartir IP ni multiaddr a mano, sólo conociendo un bootstrap común. Verificado end-to-end en localhost (rendezvous + publicador + receptor; 4 tests en `khipu-brahman`). + +La lógica vive en `khipu-share`: `net` (transporte TCP) y `discovery` (baliza UDP). 15 tests + un test de integración que recorre la cadena completa descubrir→jalar→verificar en loopback. + +## Estado (2026-05-31) + +### Hecho + +- `khipu-core` (modelo de nota + store) + `khipu-gravity` (masa/decay con refuerzo por acceso). +- `khipu-app`: UI Llimphi sobre el core, con menú principal y contextual. +- Gravedad semántica: clustering por embeddings del `verbo-daemon` (rimay), con fallback trigram 16d offline; cálculo en worker que no bloquea la UI. +- Compartir vía agora (`khipu-share`): sobres firmados Ed25519 + direccionados BLAKE3, identidad cifrada (Argon2id + ChaCha20-Poly1305 en keystore), compartir selectivo + procedencia del autor; transporte TCP/LAN + descubrimiento por baliza UDP (15 tests + integración loopback). +- WAN/P2P (`khipu-brahman` sobre libp2p/BrahmanNet): stream cifrado Noise, NAT traversal (Circuit Relay v2 + DCUtR), AutoNAT, descubrimiento por DHT Kademlia (4 tests e2e localhost). + +### Pendiente + +- Sincronización bidireccional / resolución de conflictos entre cuadernos (hoy es import unidireccional de sobres). +- Transferir física temporal opcional al compartir (hoy el contenido nace fresco en el receptor — decisión de diseño, no bug). +- Endurecimiento de la malla DHT en WAN real (probado en localhost/LAN). + +## Consideraciones + +- **No es un sistema de "todo"** — no hay due-dates ni recordatorios; es un cuaderno con física propia. +- El decay es transparente: cada nota expone su masa actual; el usuario decide si la salva. +- La gravedad es local y no transferible: compartir mueve el contenido, no la atención. diff --git a/00_unanchay/khipu/README.md b/00_unanchay/khipu/README.md new file mode 100644 index 0000000..f85f02e --- /dev/null +++ b/00_unanchay/khipu/README.md @@ -0,0 +1,30 @@ +# khipu + +> `khipu` (Quechua: knotted cord recordkeeping). Notes with temporal gravity. + +Quick-note capture where forgetting is part of the model: each note has a mass that decays with time and reinforces with each access. What's recurrent stays visible; what isn't touched fades until it falls off the horizon. + +## Install + +```sh +cargo run --release -p khipu-app +``` + +## Compatibility + +- **Linux / macOS / Windows** — Llimphi UI (Wayland/X11/Win32 via `winit`). +- Local persistence in `$XDG_DATA_HOME/khipu/`. + +## Crates + +| Crate | Role | +|---|---| +| [`khipu-core`](khipu-core/README.md) | Note model + store; no UI. | +| [`khipu-gravity`](khipu-gravity/README.md) | Mass/decay algorithm; reinforcement on access. | +| [`khipu-app`](khipu-app/README.md) | Llimphi UI over the core. | + +## Considerations + +- **It's not a "todo" system** — no due dates, no reminders; it's a notebook with its own physics. +- Decay is transparent: each note shows its current mass; the user decides whether to save it. +- Plays well with the [agora](../../03_ukupacha/agora/README.md) network: notes can be shared without losing their local gravity. diff --git a/00_unanchay/khipu/README.qu.md b/00_unanchay/khipu/README.qu.md new file mode 100644 index 0000000..0f2f69a --- /dev/null +++ b/00_unanchay/khipu/README.qu.md @@ -0,0 +1,32 @@ + + +# khipu + +> `khipu` (runa-simi: watasqa wask'akuna yuyanapaq). Qillqakuna pacha tinkuywan. + +Usqhay nota hap'iy. Qunqaytaqa modelopa ukhunpi: sapanka nota llasayuq, pachawan miran, sapa kuti rikuptin kallpachayuq. Sapa kuti rikuq nota qhipanqa; mana takyaqkuna chinkanqaku, ñawi hawamanta wikch'urunqakama. + +## Churay + +```sh +cargo run --release -p khipu-app +``` + +## Tinkuy + +- **Linux / macOS / Windows** — Llimphi UI (Wayland/X11/Win32 `winit` patapi). +- Allpa-yana waqaychay `$XDG_DATA_HOME/khipu/`-pi. + +## Crateskuna + +| Crate | Ima ruwan | +|---|---| +| [`khipu-core`](khipu-core/README.md) | Nota modelo + waqaychay; mana UI. | +| [`khipu-gravity`](khipu-gravity/README.md) | Llasay/chinkay algoritmo, rikuq kallpachay. | +| [`khipu-app`](khipu-app/README.md) | Llimphi UI core patapi. | + +## Yuyaykunaq + +- **Manan "todo" sistemachu** — mana fechakuna, mana willaqkuna; pi-físikayuq qillqana kanmi. +- Chinkayqa rikukullan: sapanka nota llasayninta rikuchin; runa salvanaqa rikun. +- [agora](../../03_ukupacha/agora/README.md) ayllu-redwan tinkuy atin: nota willanakuyqa mana lokal llasayninmanta hark'aqasqa. diff --git a/00_unanchay/khipu/khipu-app/Cargo.toml b/00_unanchay/khipu/khipu-app/Cargo.toml new file mode 100644 index 0000000..7d2580b --- /dev/null +++ b/00_unanchay/khipu/khipu-app/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "khipu-app" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "khipu-app — cuaderno de notas sobre Llimphi: lista editable in-situ + canvas de gravedad semántica con persistencia en $XDG_DATA_HOME/khipu/." + +[[bin]] +name = "khipu-app" +path = "src/main.rs" + +[[example]] +name = "demo_cli" +path = "examples/demo_cli.rs" + +[dependencies] +khipu-core = { path = "../khipu-core" } +khipu-gravity = { path = "../khipu-gravity" } +khipu-share = { path = "../khipu-share" } +khipu-brahman = { path = "../khipu-brahman" } +agora-core = { workspace = true } +rimay-verbo = { workspace = true } +tokio = { workspace = true } +llimphi-ui = { workspace = true } +llimphi-theme = { workspace = true } +llimphi-widget-list = { workspace = true } +llimphi-widget-text-editor = { workspace = true } +llimphi-widget-text-input = { workspace = true } +llimphi-widget-tiled = { workspace = true } +llimphi-widget-menubar = { workspace = true } +llimphi-widget-edit-menu = { workspace = true } +llimphi-widget-context-menu = { workspace = true } +llimphi-motion = { workspace = true } +llimphi-clipboard = { workspace = true } +app-bus = { workspace = true } +directories = { workspace = true } +postcard = { workspace = true } +serde = { workspace = true } diff --git a/00_unanchay/khipu/khipu-app/LEEME.md b/00_unanchay/khipu/khipu-app/LEEME.md new file mode 100644 index 0000000..0cf0ecf --- /dev/null +++ b/00_unanchay/khipu/khipu-app/LEEME.md @@ -0,0 +1,17 @@ +# khipu-app + +> UI Llimphi sobre el core de [khipu](../README.md). Binario del usuario. + +App de escritorio: lista de notas ordenadas por masa actual, editor inline (Markdown ligero), captura rápida (`Ctrl+N`), búsqueda fuzzy. Cada redibujo recalcula masa con [`khipu-gravity`](../khipu-gravity/README.md) y muestra notas con `mass > umbral`; las que cayeron se acceden por menú "archivo". + +## Uso + +```sh +cargo run --release -p khipu-app +``` + +## Deps + +- [`khipu-core`](../khipu-core/README.md), [`khipu-gravity`](../khipu-gravity/README.md) +- [`llimphi-ui`](../../../02_ruway/llimphi/) + widgets `text-editor`, `text-input`, `list` +- [`wawa-config-llimphi`](../../../shared/wawa-config-llimphi/) para preferencias compartidas diff --git a/00_unanchay/khipu/khipu-app/README.md b/00_unanchay/khipu/khipu-app/README.md new file mode 100644 index 0000000..afb78d4 --- /dev/null +++ b/00_unanchay/khipu/khipu-app/README.md @@ -0,0 +1,17 @@ +# khipu-app + +> Llimphi UI over the core of [khipu](../README.md). The user binary. + +Desktop app: list of notes sorted by current mass, inline editor (lightweight Markdown), quick capture (`Ctrl+N`), fuzzy search. Each redraw recomputes mass via [`khipu-gravity`](../khipu-gravity/README.md) and shows notes with `mass > threshold`; fallen ones are accessed via the "archive" menu. + +## Usage + +```sh +cargo run --release -p khipu-app +``` + +## Deps + +- [`khipu-core`](../khipu-core/README.md), [`khipu-gravity`](../khipu-gravity/README.md) +- [`llimphi-ui`](../../../02_ruway/llimphi/) + widgets `text-editor`, `text-input`, `list` +- [`wawa-config-llimphi`](../../../shared/wawa-config-llimphi/) for shared prefs diff --git a/00_unanchay/khipu/khipu-app/examples/demo_cli.rs b/00_unanchay/khipu/khipu-app/examples/demo_cli.rs new file mode 100644 index 0000000..553f50a --- /dev/null +++ b/00_unanchay/khipu/khipu-app/examples/demo_cli.rs @@ -0,0 +1,159 @@ +//! `demo_cli` — demostración no gráfica del cuaderno. +//! +//! 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 khipu-app --example demo_cli --release`. + +use khipu_core::{NoteId, NoteStore}; +use khipu_gravity::{Gravity, GravityConfig, Params, 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 { + 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(); + + let seed: [(&str, &str, &[&str], Vec); 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 khipu · 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!("\n masa temporal — vida media 7 días, boost 0.4, horizonte 0.10:"); + let g = Gravity::new(Params::default()); + let pivot_b = ids[4].0; // "Semillas de cilantro" + println!(" «{}» — escenario:", name(pivot_b)); + let mut mass = 1.0_f32; + println!(" inicial mass = {:.3}", mass); + mass = g.decay(mass, 3.0 * 24.0 * 3600.0); + println!( + " 3 días sin acceso → decay mass = {:.3} ({})", + mass, + if g.is_visible(mass) { "visible" } else { "archivo" } + ); + mass = g.decay(mass, 14.0 * 24.0 * 3600.0); + println!( + " +14 días sin acceso → decay mass = {:.3} ({})", + mass, + if g.is_visible(mass) { "visible" } else { "archivo" } + ); + mass = g.reinforce(mass); + println!( + " el usuario abre la nota → reinforce mass = {:.3} ({})", + mass, + if g.is_visible(mass) { "visible" } else { "archivo" } + ); + 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(", ") + } +} diff --git a/00_unanchay/khipu/khipu-app/src/main.rs b/00_unanchay/khipu/khipu-app/src/main.rs new file mode 100644 index 0000000..19366fd --- /dev/null +++ b/00_unanchay/khipu/khipu-app/src/main.rs @@ -0,0 +1,1928 @@ +//! `khipu-app` — cuaderno de notas sobre Llimphi. +//! +//! Tres regiones, todas en la misma ventana, sin modal: +//! - **Lista** (izquierda, 240 px): notas en orden de creación. +//! Click selecciona. Botón `+ nueva` arriba. +//! - **Editor** (centro): título (input), cuerpo (text-editor con +//! wiki-links `[[...]]`), etiquetas (input). Edición directa — la +//! nota seleccionada se modifica al teclear, sin botón guardar. +//! - **Gravedad** (derecha): canvas vello que pinta las posiciones +//! 2D del [`SemanticField::gravity_layout`]. Color por clúster +//! (umbral 0.55), la seleccionada va resaltada con borde acento. +//! +//! **Embeddings**: si hay un `verbo-daemon` corriendo en el socket por +//! defecto (`$XDG_RUNTIME_DIR/verbo.sock`) los vectores son reales +//! (fastembed e5, etc.) — clústeres y vecinos se vuelven semánticos de +//! verdad. Sin daemon caemos al hash trigram → R^16 local (random +//! projection 1-bit signed, normalizado): determinista, offline, +//! idéntico al comportamiento histórico. Ver [`Embedder`]. El cálculo +//! es async, así que viaja a un worker (`Handle::spawn`) y reentra al +//! `update` con [`Msg::EmbeddingReady`] — la UI nunca se bloquea. +//! +//! **Persistencia**: cada mutación graba `$XDG_DATA_HOME/khipu/notes.bin` +//! con postcard, anotando la etiqueta del espacio vectorial usado. Al +//! arrancar, si el archivo existe se carga; si el espacio cambió (otro +//! modelo o dimensión) los vectores se recalculan. Sin archivo se +//! siembra el cuaderno demo (siete notas en español). + +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use agora_core::Keypair; +use directories::ProjectDirs; +use khipu_share::SignedBundle; +use rimay_verbo::Provider; +use khipu_core::{Note, NoteId, NoteStore}; +use khipu_gravity::{Gravity, Params, SemanticField}; +use llimphi_theme::Theme; +use llimphi_ui::llimphi_hal::winit::keyboard::{Key, NamedKey}; +use llimphi_ui::llimphi_layout::taffy::{ + prelude::{length, percent, FlexDirection, Rect, Size, Style}, + AlignItems, Dimension, JustifyContent, +}; +use llimphi_ui::llimphi_text::Alignment; +use llimphi_ui::{App, Handle, KeyEvent, KeyState, View}; +use llimphi_widget_list::ListPalette; +use llimphi_widget_text_editor::{EditorMetrics, EditorPalette, EditorState, PointerEvent}; +use llimphi_widget_text_input::{text_input_view, TextInputPalette, TextInputState}; +use llimphi_widget_menubar::{ + menubar_command_at, menubar_nav, menubar_overlay_animated, menubar_view, MenuBarSpec, + DEFAULT_HEIGHT as MENU_H, +}; +use llimphi_widget_edit_menu::{self as editmenu, EditAction, EditFlags}; +use llimphi_widget_context_menu::{context_menu_view_ex, ContextMenuExtras}; +use llimphi_motion::{animate, motion, Tween}; +use llimphi_clipboard::SystemClipboard; +use serde::{Deserialize, Serialize}; + +mod map; +mod net; +mod panels; +use map::*; +use net::*; +use panels::*; + +/// Dimensión del embebedor local (fallback sin daemon). +const EMBED_DIM: usize = 16; +const CLUSTER_THRESHOLD: f32 = 0.55; +const EDITOR_VISIBLE_LINES: usize = 24; +const LIST_WIDTH: f32 = 240.0; +/// Ancho del editor flotante (overlay derecho sobre el mapa). +const EDITOR_OVERLAY_W: f32 = 420.0; +/// Zoom a partir del cual el nodo seleccionado deja de editarse en el panel +/// lateral y pasa a abrirse como tarjeta anclada a su coordenada en el mapa +/// (zoom semántico). Por debajo, el editor vuelve al overlay derecho. +const ZOOM_INJECT: f32 = 1.6; +const HEADER_H: f32 = 36.0; +const ROW_H: f32 = 26.0; +const FIELD_LABEL_SIZE: f32 = 10.0; + +/// Fuente de vectores semánticos. Con un `verbo-daemon` en el socket por +/// defecto usa embeddings reales; si no hay daemon cae al hash-trigram +/// local de 16d — determinista, offline, sin runtime. +/// +/// El arm remoto guarda el `Runtime` de tokio para resolver las llamadas +/// async del `Provider` con `block_on` desde el hilo worker que las +/// dispara (nunca el de UI). Es `Clone` (todo tras `Arc`) para viajar +/// barato dentro de la closure de `Handle::spawn`. +#[derive(Clone)] +enum Embedder { + /// Daemon `rimay-verbo` por socket Unix. + Remote { + provider: Arc, + rt: Arc, + dim: usize, + label: String, + }, + /// Fallback local: hash-trigram → R^EMBED_DIM, sin red ni runtime. + Local, +} + +impl Embedder { + /// Conecta al `verbo-daemon` en el socket por defecto. Si no hay + /// ninguno (o no se pudo armar el runtime), devuelve el embebedor + /// local — los demos arrancan igual, sin red. + fn connect() -> Self { + let rt = match tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + { + Ok(rt) => rt, + Err(_) => return Embedder::Local, + }; + match rt.block_on(rimay_verbo::conectar()) { + Ok(client) => { + let id = client.model_id(); + let dim = id.dimension; + let label = id.to_string(); + Embedder::Remote { + provider: Arc::new(client), + rt: Arc::new(rt), + dim, + label, + } + } + Err(_) => Embedder::Local, + } + } + + /// Etiqueta del espacio vectorial. Si cambia entre dos arranques, los + /// vectores persistidos son incomparables (otro modelo o dimensión) y + /// hay que recalcularlos — ver [`from_state`]. + fn label(&self) -> String { + match self { + Embedder::Remote { label, .. } => label.clone(), + Embedder::Local => format!("khipu-trigram-{EMBED_DIM}d"), + } + } + + /// Embebe `text` de forma bloqueante. En el arm remoto resuelve el + /// future con `block_on`; ante un error del backend devuelve un + /// vector de ceros (afinidad nula con todo, nunca panic). + fn embed_blocking(&self, text: &str) -> Vec { + match self { + Embedder::Local => embed(text, EMBED_DIM), + Embedder::Remote { provider, rt, dim, .. } => rt + .block_on(provider.embed(text)) + .map(|v| v.values) + .unwrap_or_else(|_| vec![0.0; *dim]), + } + } +} + +/// Foco activo del teclado. Cualquier `KeyEvent` se rutea al input +/// correspondiente; sin foco las teclas se ignoran. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Focus { + None, + Search, + Title, + Body, + Tags, + Passphrase, + PeerAddr, + /// Input del nombre de una región emergente (bautizo de un clúster). + Region, +} + +/// Una región del mapa: un nombre pinchado en una coordenada de mundo. No +/// es una carpeta — es un topónimo. Nace cuando el usuario bautiza un +/// clúster denso que el mapa detectó, y de ahí queda como landmark fijo; +/// los pensamientos cercanos "pertenecen" a esa zona por vecindad, no por +/// asignación. Las placas tectónicas emergen del caos, no se imponen. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Region { + name: String, + x: f32, + y: f32, +} + +/// Nodo libp2p del cuaderno + su runtime tokio (que lo mantiene vivo). Se +/// arma perezosamente la primera vez que se usa P2P (`ensure_p2p`). +struct P2p { + rt: Arc, + node: Arc, + /// Nuestra dirección para compartir (`/ip4/.../tcp/.../p2p/`). + dial_addr: String, + /// `true` cuando ya estamos sirviendo el cuaderno por libp2p. + serving: bool, +} + +/// Un par descubierto en la red, en forma lista para la UI: dónde jalarle +/// el cuaderno y una etiqueta legible. Datos planos (no `PeerVisto`) para +/// viajar dentro de un `Msg`. +#[derive(Clone)] +struct PeerInfo { + /// Dirección TCP de fetch, como string (`ip:puerto`). + addr: String, + /// Etiqueta para la fila: nombre · de:autor · dirección. + label: String, +} + +#[derive(Clone)] +enum Msg { + SelectNote(NoteId), + NewNote, + DeleteSelected, + ToggleArchive, + Focus(Focus), + Key(KeyEvent), + EditorPointer(PointerEvent), + /// Latido — fuerza el rerender para que la masa decaiga + /// visiblemente aunque el usuario no esté tocando nada. + Tick, + /// Resultado async de un embed: `(nota, secuencia, vector)`. Se + /// aplica sólo si `secuencia` sigue siendo la más reciente para esa + /// nota — descarta cálculos que ediciones posteriores dejaron viejos. + EmbeddingReady(NoteId, u64, Vec), + /// Sella todo el cuaderno en un sobre firmado (`compartido.khipu`). + Export, + /// Verifica e ingiere `compartido.khipu` como notas nuevas. + Import, + /// Empieza a servir el cuaderno por TCP para que un par lo jale. + Publish, + /// Busca pares en la LAN para jalarles el cuaderno. + Receive, + /// Resultado del descubrimiento: los pares vistos (ya sin uno mismo). + PeersFound(Vec), + /// Jala el cuaderno del par en esta dirección TCP. + FetchFrom(String), + /// Jala de la dirección escrita a mano (input) — habilita WAN: cualquier + /// `host:puerto` alcanzable, no sólo pares descubiertos en la LAN. + FetchManual, + /// Cierra el panel de recibir sin jalar nada. + CancelPeers, + /// Resultado async de un fetch: el sobre recibido o un error. + Received(Result), + /// Intenta desbloquear la identidad con la passphrase tipeada. + Unlock, + /// Cierra el prompt de passphrase sin desbloquear. + CancelUnlock, + /// Resultado async de reservar un circuito en un relay: la dirección de + /// marcado vía circuito (o un mensaje de error) para mostrar. + RelayReady(String), + /// Abre/cierra un dropdown de la barra de menú principal (índice del menú). + MenuOpen(Option), + /// Comando elegido en la barra de menú (`command` de cada `MenuItem`). + MenuCommand(String), + /// Navegación por teclado en el menú principal (`+1` baja, `-1` sube). + MenuNav(i32), + /// Enter en el menú principal: ejecuta la fila activa. + MenuActivate, + /// Tick de animación de menús (sólo re-render). + MenuTick, + /// Navegación por teclado en el menú de edición. + EditNav(i32), + /// Enter en el menú de edición: ejecuta la fila activa. + EditActivate, + /// Abre el menú de edición contextual (right-click) en coords de ventana. + EditMenuOpen(f32, f32), + /// Acción de edición (undo/redo/cut/copy/paste/delete/selectall) sobre + /// el campo focuseado. + EditMenuAction(EditAction), + /// Cierra cualquier menú abierto (principal o de edición). + CloseMenus, + /// Pan del mapa: delta de arrastre en pixels de pantalla. + MapPan(f32, f32), + /// Zoom del mapa: delta de rueda (líneas, signo winit). Acerca/aleja. + MapZoom(f32), + /// Click en el lienzo en coords locales `(lx, ly)` sobre un rect + /// `(w, h)`: selecciona la nota más cercana bajo el cursor, si hay. + MapClick(f32, f32, f32, f32), + /// Abre/cierra el cajón de notas (overlay izquierdo). + ToggleList, + /// Cierra el editor flotante: deselecciona y vuelve al mapa limpio. + Deselect, + /// Escape en el mapa: cierra lo de más arriba (editor → cajón → foco). + EscapeMap, + /// Empieza a bautizar una región en la coordenada de mundo `(x, y)` + /// (el centroide del clúster denso que se ofreció nombrar). + BeginNaming(f32, f32), + /// Confirma el bautizo: crea la región con el texto tipeado. + CommitNaming, + /// Cancela el bautizo sin crear región. + CancelNaming, +} + +struct Model { + store: NoteStore, + field: SemanticField, + /// Orden de inserción (estable). La presentación se reordena por + /// masa decreciente al renderizar. + order: Vec, + selected: Option, + title: TextInputState, + body: EditorState, + tags: TextInputState, + search: TextInputState, + focus: Focus, + theme: Theme, + data_path: Option, + /// Física temporal: vida media + boost + horizonte. + gravity: Gravity, + /// `true` cuando el usuario quiere ver también las notas que + /// cayeron del horizonte. Default `false`. + show_archive: bool, + /// Fuente de embeddings: daemon `verbo` o fallback trigram local. + embedder: Embedder, + /// Última secuencia de embedding pedida por nota. Un resultado async + /// (`Msg::EmbeddingReady`) sólo se aplica si su secuencia coincide + /// con la vigente aquí; así una edición rápida invalida el cálculo + /// de la anterior sin condición de carrera. + embed_latest: BTreeMap, + /// Contador monótono de pedidos de embedding. + embed_seq: u64, + /// Identidad Ed25519 del cuaderno, para firmar/exportar sobres + /// (`khipu-share`). `None` si no hay directorio de datos. + keypair: Option, + /// Última línea de estado (export/import/red). Se pinta en una barra + /// al pie cuando es `Some`. + status: Option, + /// `true` cuando ya hay un servidor TCP sirviendo el cuaderno. Evita + /// rebindear el puerto si se pulsa «publicar» dos veces. + publishing: bool, + /// `true` mientras el panel izquierdo está en modo "recibir": input de + /// dirección + lista de pares descubiertos. + receiving: bool, + /// Pares descubiertos en la última búsqueda (filas clickeables). + peers: Vec, + /// Dirección manual del par para recibir (habilita WAN). Prellenada + /// con `KHIPU_PEER` o el default; editable. + peer_input: TextInputState, + /// Input de la passphrase para desbloquear la identidad. + passphrase: TextInputState, + /// `true` mientras se muestra el prompt de passphrase (modal). + unlocking: bool, + /// Acción a reanudar tras desbloquear (lo que el usuario quiso hacer + /// y disparó el prompt). Se redispatcha al lograr el unlock. + pending: Option>, + /// Nodo libp2p (perezoso): `Some` una vez que se usó P2P. + p2p: Option, + /// Dropdown abierto de la barra de menú (índice), o `None` si cerrada. + menu_open: Option, + /// Fila resaltada por teclado en el menú principal (`usize::MAX` = ninguna). + menu_active: usize, + /// Animación de aparición/swap del dropdown del menú principal (0→1). + menu_anim: Tween, + /// Posición (coords de ventana) del menú de edición contextual, si abierto. + edit_menu: Option<(f32, f32)>, + /// Fila resaltada por teclado en el menú de edición (`usize::MAX` = ninguna). + edit_active: usize, + /// Animación de aparición del menú de edición (0→1). + edit_anim: Tween, + /// Portapapeles del sistema, compartido por todas las acciones de edición. + clipboard: SystemClipboard, + /// Desplazamiento de la cámara del mapa, en coordenadas de mundo. El + /// lienzo es infinito; arrastrar el fondo desplaza este vector. + cam_pan: (f32, f32), + /// Escala de la cámara del mapa (1.0 = mundo:pantalla). La rueda la + /// cambia; el zoom semántico futuro decidirá qué se inyecta según ella. + cam_zoom: f32, + /// `true` mientras el cajón de notas (overlay izquierdo) está abierto. + /// El mapa es la interfaz; la lista es un cajón invocable, no un panel + /// permanente. Default `true` para no perder al usuario en el primer + /// arranque. + show_list: bool, + /// Último tamaño conocido del lienzo `(w, h)` en pixels. Lo aprende de + /// cada click (`on_click_at` lo trae) y sirve para anclar la tarjeta + /// del nodo en su coordenada de pantalla durante el zoom semántico, + /// que se calcula en `view()` antes de que corra el layout. Se corrige + /// solo en el siguiente click tras un resize. + canvas_size: (f32, f32), + /// Topónimos del mapa: regiones bautizadas (landmarks persistidos). + regions: Vec, + /// Coordenada de mundo de la región que se está bautizando ahora mismo + /// (input abierto), o `None`. + naming: Option<(f32, f32)>, + /// Input del nombre de la región en curso. + region_input: TextInputState, +} + +struct KhipuApp; + +impl App for KhipuApp { + type Model = Model; + type Msg = Msg; + + fn init(handle: &Handle) -> Model { + // Conectamos al daemon una sola vez al arrancar; el embebedor + // resultante (remoto o local) se clona barato a cada worker. + let embedder = Embedder::connect(); + let data_path = data_file_path(); + let mut model = match data_path.as_ref().and_then(load_state) { + Some(state) => from_state(state, embedder), + None => seeded_model(embedder), + }; + model.data_path = data_path; + // Identidad: si `KHIPU_PASSPHRASE` está en el entorno, desbloqueamos + // (o creamos/migramos) sin prompt — útil headless. Si no, queda + // bloqueada y se pide la passphrase al primer intento de compartir. + model.keypair = std::env::var("KHIPU_PASSPHRASE") + .ok() + .and_then(|p| unlock_identity(&p)); + model.theme = Theme::dark(); + // Con bootstrap configurado, arrancamos el nodo libp2p ya, para que + // la malla DHT esté caliente cuando el usuario quiera descubrir. + if std::env::var("KHIPU_BOOTSTRAP").is_ok() { + ensure_p2p(&mut model); + } + // Elegimos la primera nota más pesada (decayendo on-the-fly); + // si todo el cuaderno está en archivo, caemos al orden de + // inserción para no abrir vacío. + let first = first_visible(&model).or_else(|| model.order.first().copied()); + if let Some(id) = first { + reinforce_and_touch(&mut model, id); + select(&mut model, id); + } + persist(&model); + // Latido cada 30 s — la masa decae en disco como en pantalla. + handle.spawn_periodic(std::time::Duration::from_secs(30), || Msg::Tick); + model + } + + fn update(mut model: Model, msg: Msg, h: &Handle) -> Model { + match msg { + Msg::SelectNote(id) => { + commit_edits(&mut model, h); + reinforce_and_touch(&mut model, id); + select(&mut model, id); + persist(&model); + } + Msg::NewNote => { + commit_edits(&mut model, h); + let now = now_secs(); + let id = model.store.create("Nota nueva", "", Vec::new(), now); + model.order.push(id); + schedule_embedding(&mut model, id, h); + select(&mut model, id); + persist(&model); + } + Msg::ToggleArchive => { + model.show_archive = !model.show_archive; + } + Msg::Tick => { + // No muta nada: la masa vive en `current_mass` (decay + // contra `last_access`). El Tick existe sólo para + // pedirle al event loop un redraw. + } + Msg::EmbeddingReady(id, seq, v) => { + // Aplicamos el vector sólo si sigue siendo el cálculo más + // reciente para esa nota y la nota no fue borrada entre + // medio. Tras insertarlo, persistimos para que el campo + // semántico en disco quede al día. + if model.embed_latest.get(&id) == Some(&seq) + && model.store.get(id).is_some() + { + model.field.insert(id, v); + // Recién ahora la nota tiene vector: le damos domicilio + // en el mapa una sola vez, cerca de sus parientes. Si ya + // tenía posición (re-embed por edición), no se mueve. + place_note(&mut model, id); + persist(&model); + } + } + Msg::Export => { + // Firmar requiere identidad: si está bloqueada, pedimos la + // passphrase y reanudamos el export al desbloquear. + if model.keypair.is_none() { + start_unlock(&mut model, Msg::Export); + } else { + commit_edits(&mut model, h); + model.status = Some(export_notebook(&model)); + } + } + Msg::Import => { + let report = import_notebook(&mut model, h); + persist(&model); + model.status = Some(report); + } + Msg::Publish => { + if model.keypair.is_none() { + start_unlock(&mut model, Msg::Publish); + } else { + // Asegura que el sobre en disco refleje lo editado, luego + // levanta (una vez) el servidor TCP que lo sirve. + commit_edits(&mut model, h); + let _ = export_notebook(&model); + model.status = Some(start_publishing(&mut model, h)); + } + } + Msg::Receive => { + let my_key = model.keypair.as_ref().map(|k| k.public_key()); + // Abrimos el panel de recibir ya: input de dirección + // (prellenado, editable para WAN) + lista que se irá + // poblando con lo que aparezca en la LAN. + model.receiving = true; + model.peers.clear(); + if model.peer_input.is_empty() { + model.peer_input.set_text(peer_addr()); + } + model.focus = Focus::PeerAddr; + model.status = Some("buscando pares (LAN + DHT)… o escribí una dirección".into()); + // Si hay nodo libp2p (bootstrap configurado), también + // consultamos la DHT; lo capturamos para el worker. + let dht = model.p2p.as_ref().map(|p| (p.rt.clone(), p.node.clone())); + // El descubrimiento bloquea: va a un worker y reentra con la + // lista de pares (LAN por UDP + DHT por libp2p, sin uno mismo). + h.spawn(move || { + let mut infos: Vec = + khipu_share::discovery::descubrir(std::time::Duration::from_secs(3)) + .unwrap_or_default() + .into_iter() + .filter(|p| Some(p.beacon.author) != my_key) + .map(|p| PeerInfo { + addr: p.fetch_addr.to_string(), + label: format!( + "LAN · {} · de:{} · {}", + p.beacon.name, + khipu_share::hex8(&p.beacon.author), + p.fetch_addr + ), + }) + .collect(); + if let Some((rt, node)) = dht { + let me = node.peer_id(); + for pid in rt.block_on(node.descubrir()) { + if pid == me { + continue; + } + let s = pid.to_string(); + let corto: String = s.chars().rev().take(8).collect::>() + .into_iter().rev().collect(); + infos.push(PeerInfo { + label: format!("DHT · …{corto}"), + addr: s, + }); + } + } + Msg::PeersFound(infos) + }); + } + Msg::PeersFound(peers) => { + // Sólo aplica si seguimos en modo recibir (no cancelado). + if model.receiving { + model.status = Some(if peers.is_empty() { + "ningún par en la LAN — escribí una dirección y jalá".into() + } else { + format!("{} pares en la red — elegí uno o escribí una dirección", peers.len()) + }); + model.peers = peers; + } + } + Msg::FetchManual => { + let addr = model.peer_input.text().trim().to_string(); + if addr.is_empty() { + model.status = Some("escribí una dirección host:puerto".into()); + } else { + h.dispatch(Msg::FetchFrom(addr)); + } + } + Msg::FetchFrom(addr) => { + model.receiving = false; + model.peers.clear(); + model.focus = Focus::None; + let destino = addr.trim().to_string(); + if destino.starts_with('/') || !destino.contains(':') { + // Vía libp2p: multiaddr (`/ip4/…/p2p/`, incluido + // circuito) o un peer-id pelado (descubierto por DHT). + // Arma el nodo si hace falta. + if ensure_p2p(&mut model) { + let p = model.p2p.as_ref().expect("p2p recién armado"); + let (rt, node) = (p.rt.clone(), p.node.clone()); + let es_multiaddr = destino.starts_with('/'); + model.status = Some(format!("jalando por libp2p de {destino}…")); + h.spawn(move || { + let res = if es_multiaddr { + rt.block_on(node.fetch_addr_str(&destino)) + } else { + rt.block_on(node.fetch_peer_str(&destino)) + }; + match res { + Ok(s) => Msg::Received(Ok(s)), + Err(e) => Msg::Received(Err(format!("p2p: {e}"))), + } + }); + } else { + model.status = Some("no se pudo iniciar el nodo libp2p".into()); + } + } else { + // Dirección TCP `host:puerto` (LAN/WAN directa). + model.status = Some(format!("jalando de {destino}…")); + h.spawn(move || match khipu_share::net::fetch(&destino) { + Ok(s) => Msg::Received(Ok(s)), + Err(e) => { + Msg::Received(Err(format!("no se pudo recibir de {destino}: {e}"))) + } + }); + } + } + Msg::CancelPeers => { + model.receiving = false; + model.peers.clear(); + model.focus = Focus::None; + model.status = Some("recibir cancelado".into()); + } + Msg::Received(res) => { + model.receiving = false; + model.peers.clear(); + model.status = Some(match res { + Ok(sobre) => match khipu_share::open(&sobre) { + Ok(bundle) => { + let now = now_secs(); + let outcome = + khipu_share::import_into(&mut model.store, bundle, now); + for id in &outcome.created { + model.order.push(*id); + schedule_embedding(&mut model, *id, h); + } + persist(&model); + format!( + "recibidas {} · omitidas {} (ya existían)", + outcome.created.len(), + outcome.skipped + ) + } + Err(_) => "firma inválida — sobre rechazado".into(), + }, + Err(e) => e, + }); + } + Msg::Unlock => { + let pass = model.passphrase.text(); + match unlock_identity(&pass) { + Some(kp) => { + let id = khipu_share::hex8(&kp.public_key()); + model.keypair = Some(kp); + model.unlocking = false; + model.passphrase.clear(); + model.focus = Focus::None; + model.status = Some(format!("identidad desbloqueada · {id}")); + // Reanudar lo que el usuario quería hacer. + if let Some(accion) = model.pending.take() { + h.dispatch(*accion); + } + } + None => { + model.status = + Some("passphrase incorrecta o sin acceso al keystore".into()); + } + } + } + Msg::CancelUnlock => { + model.unlocking = false; + model.pending = None; + model.passphrase.clear(); + model.focus = Focus::None; + model.status = Some("desbloqueo cancelado".into()); + } + Msg::RelayReady(addr) => { + model.status = Some(format!("alcanzable vía relay: {addr}")); + } + Msg::DeleteSelected => { + if let Some(id) = model.selected { + model.store.remove(id); + model.order.retain(|x| *x != id); + model.field.remove(id); + let next = model.order.first().copied(); + model.selected = None; + model.title.clear(); + model.body = EditorState::default(); + model.tags.clear(); + if let Some(n) = next { + select(&mut model, n); + } + persist(&model); + } + } + Msg::Focus(f) => { + commit_edits(&mut model, h); + model.focus = f; + } + Msg::Key(ev) => { + let changed = match model.focus { + Focus::Title => model.title.apply_key(&ev), + Focus::Body => model.body.apply_key(&ev).touched(), + Focus::Tags => model.tags.apply_key(&ev), + Focus::Search => { + // El search no muta el store: filtramos al + // renderizar. Sólo consumimos el evento. + let _ = model.search.apply_key(&ev); + false + } + Focus::Passphrase => { + // La passphrase no toca el store; sólo el input. + let _ = model.passphrase.apply_key(&ev); + false + } + Focus::PeerAddr => { + let _ = model.peer_input.apply_key(&ev); + false + } + Focus::Region => { + let _ = model.region_input.apply_key(&ev); + false + } + Focus::None => false, + }; + if changed { + commit_edits(&mut model, h); + } + } + Msg::EditorPointer(ev) => { + let metrics = EditorMetrics::for_font_size(13.0); + match ev { + PointerEvent::Click { x, y } => { + let (line, col) = metrics.screen_to_pos(x, y, model.body.scroll_offset); + model.body.set_caret_at(line, col); + } + PointerEvent::Drag { initial_x, initial_y, dx, dy } => { + let (l0, c0) = metrics.screen_to_pos( + initial_x, + initial_y, + model.body.scroll_offset, + ); + let (l1, c1) = metrics.screen_to_pos( + initial_x + dx, + initial_y + dy, + model.body.scroll_offset, + ); + model.body.set_caret_at(l0, c0); + model.body.extend_selection_to(l1, c1); + } + } + model.focus = Focus::Body; + } + Msg::MenuOpen(idx) => { + model.menu_open = idx; + model.menu_active = usize::MAX; + model.edit_menu = None; + if idx.is_some() { + model.menu_anim = Tween::new(0.0, 1.0, motion::FAST, motion::ease_out_cubic); + animate(h, motion::FAST, || Msg::MenuTick); + } + } + Msg::MenuCommand(cmd) => { + return handle_menu_command(model, cmd, h); + } + Msg::MenuNav(dir) => { + if let Some(mi) = model.menu_open { + let menu = app_menu(&model); + model.menu_active = menubar_nav(&menu, mi, model.menu_active, dir); + } + } + Msg::MenuActivate => { + if let Some(mi) = model.menu_open { + let menu = app_menu(&model); + if let Some(cmd) = menubar_command_at(&menu, mi, model.menu_active) { + return handle_menu_command(model, cmd, h); + } + } + } + Msg::MenuTick => {} + Msg::EditNav(dir) => { + let flags = focused_edit_flags(&model); + model.edit_active = editmenu::edit_menu_step(flags, model.edit_active, dir); + } + Msg::EditActivate => { + let flags = focused_edit_flags(&model); + if let Some(action) = editmenu::edit_menu_action_at(flags, model.edit_active) { + return apply_edit_menu_action(model, action, h); + } + } + Msg::EditMenuOpen(x, y) => { + model.edit_menu = Some((x, y)); + model.edit_active = usize::MAX; + model.menu_open = None; + model.edit_anim = Tween::new(0.0, 1.0, motion::FAST, motion::ease_out_cubic); + animate(h, motion::FAST, || Msg::MenuTick); + } + Msg::EditMenuAction(action) => { + return apply_edit_menu_action(model, action, h); + } + Msg::CloseMenus => { + model.menu_open = None; + model.menu_active = usize::MAX; + model.edit_menu = None; + model.edit_active = usize::MAX; + } + Msg::MapPan(dx, dy) => { + // El delta viene en pixels de pantalla; lo llevamos a + // mundo dividiendo por el zoom para que el arrastre se + // sienta 1:1 con el cursor a cualquier escala. + let z = model.cam_zoom.max(0.01); + model.cam_pan.0 += dx / z; + model.cam_pan.1 += dy / z; + } + Msg::MapZoom(dy) => { + // dy>0 = rueda hacia el usuario (winit invierte el signo en + // el event loop) → alejar. Factor multiplicativo, clamp para + // no perder el mapa. + let factor = (1.0 - dy * 0.12).clamp(0.5, 2.0); + model.cam_zoom = (model.cam_zoom * factor).clamp(0.15, 6.0); + } + Msg::MapClick(lx, ly, rw, rh) => { + model.canvas_size = (rw, rh); + if let Some(id) = pick_note(&model, lx, ly, rw, rh) { + commit_edits(&mut model, h); + reinforce_and_touch(&mut model, id); + select(&mut model, id); + persist(&model); + } + } + Msg::ToggleList => { + model.show_list = !model.show_list; + } + Msg::Deselect => { + commit_edits(&mut model, h); + deselect(&mut model); + persist(&model); + } + Msg::BeginNaming(x, y) => { + model.naming = Some((x, y)); + model.region_input.clear(); + model.focus = Focus::Region; + } + Msg::CommitNaming => { + if let Some((x, y)) = model.naming.take() { + let name = model.region_input.text().trim().to_string(); + if !name.is_empty() { + model.regions.push(Region { name, x, y }); + persist(&model); + } + } + model.region_input.clear(); + model.focus = Focus::None; + } + Msg::CancelNaming => { + model.naming = None; + model.region_input.clear(); + model.focus = Focus::None; + } + Msg::EscapeMap => { + // Cierra la capa más cercana al usuario, en orden. + if model.naming.is_some() { + model.naming = None; + model.region_input.clear(); + model.focus = Focus::None; + } else if model.selected.is_some() { + commit_edits(&mut model, h); + deselect(&mut model); + persist(&model); + } else if model.show_list { + model.show_list = false; + } else { + model.focus = Focus::None; + } + } + } + model + } + + fn view(model: &Model) -> View { + let palette = ListPalette::from_theme(&model.theme); + let input_palette = TextInputPalette::from_theme(&model.theme); + let editor_palette = EditorPalette::from_theme(&model.theme); + + // Prompt de passphrase: ocupa toda la ventana hasta resolverse. + if model.unlocking { + let mut children = vec![header_view(model), unlock_view(model, &input_palette)]; + if let Some(bar) = status_bar(model) { + children.push(bar); + } + return View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_app) + .children(children); + } + + let header = header_view(model); + + // Zoom semántico: con una nota seleccionada y el mapa lo bastante + // cerca, el nodo se "abre" como tarjeta anclada a su coordenada + // (in-situ). Lejos, el editor cae al panel lateral — un fallback + // para editar sin tener que acercarse. + let inplace = model + .selected + .filter(|_| model.cam_zoom >= ZOOM_INJECT) + .and_then(|id| node_screen_pos(model, id).map(|p| (id, p))); + + // El mapa es la interfaz: ocupa todo el cuerpo como capa de fondo. + // Sobre él viajan, como hijos del canvas: la tarjeta del nodo + // abierto (zoom semántico), los chips para bautizar clústeres + // densos, y el input del bautizo en curso. + let mut injected: Vec> = Vec::new(); + if let Some((_, (nx, ny))) = inplace { + let editor = editor_panel(model, &input_palette, &editor_palette); + injected.push(node_card(editor, nx, ny, model.canvas_size, &model.theme)); + } + // Sugerencias de bautizo (sólo si no estamos editando in-situ, para + // no encimar la tarjeta). + if inplace.is_none() { + for (wx, wy) in unnamed_cluster_centroids(model) { + let (sx, sy) = world_screen(model, wx, wy); + injected.push(pinned( + name_region_chip(wx, wy, &model.theme), + sx, + sy, + 132.0, + 24.0, + model.canvas_size, + )); + } + } + // Input del bautizo en curso, anclado al centroide elegido. + if let Some((wx, wy)) = model.naming { + let (sx, sy) = world_screen(model, wx, wy); + injected.push(pinned( + naming_input(model, &input_palette), + sx, + sy, + 220.0, + 34.0, + model.canvas_size, + )); + } + let map = gravity_panel(model, injected); + let mut layers: Vec> = vec![map]; + + // Cajón de notas (izquierda): abierto a pedido, o forzado en modo + // recibir (muestra los pares en vez de las notas). + if model.show_list || model.receiving { + let drawer = if model.receiving { + receive_panel(model, &palette, &input_palette) + } else { + list_panel(model, &palette, &input_palette) + }; + layers.push(overlay_left(drawer, LIST_WIDTH)); + } + + // Editor lateral: sólo si hay selección y NO se abrió in-situ. + if model.selected.is_some() && inplace.is_none() { + let editor = editor_panel(model, &input_palette, &editor_palette); + layers.push(overlay_right(editor, EDITOR_OVERLAY_W, &model.theme)); + } + + let body = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .children(layers); + + // Barra de menú principal: primer hijo del column raíz. + let menu = app_menu(model); + let menubar = menubar_view(&menubar_spec(&menu, model, &model.theme)); + + let mut children = vec![menubar, header, body]; + if let Some(bar) = status_bar(model) { + children.push(bar); + } + + // El right-click se engancha en la raíz (origen 0,0 → coords + // locales == coords de ventana) y abre el menú de edición sobre + // el campo focuseado. + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_app) + .on_right_click_at(|x, y, _w, _h| Some(Msg::EditMenuOpen(x, y))) + .children(children) + } + + fn view_overlay(model: &Model) -> Option> { + // El prompt modal de passphrase no convive con menús. + if model.unlocking { + return None; + } + // Prioridad: menú de edición contextual sobre el menú principal. + if let Some((x, y)) = model.edit_menu { + let flags = focused_edit_flags(model); + let (w, h) = Self::initial_size(); + let mut spec = editmenu::edit_context_menu( + (x, y), + (w as f32, h as f32), + &model.theme, + flags, + Msg::EditMenuAction, + Msg::CloseMenus, + ); + spec.active = model.edit_active; + return Some(context_menu_view_ex( + spec, + ContextMenuExtras { + appear: model.edit_anim.value(), + ..Default::default() + }, + )); + } + // Si no, el dropdown del menú principal. + let menu = app_menu(model); + menubar_overlay_animated( + &menubar_spec(&menu, model, &model.theme), + model.menu_active, + model.menu_anim.value(), + ) + } + + fn on_key(model: &Model, event: &KeyEvent) -> Option { + // Menús abiertos: las flechas navegan y tienen prioridad sobre todo. + if event.state == KeyState::Pressed { + if let Some(mi) = model.menu_open { + let n = app_menu(model).menus.len().max(1); + return match &event.key { + Key::Named(NamedKey::Escape) => Some(Msg::CloseMenus), + Key::Named(NamedKey::ArrowLeft) => Some(Msg::MenuOpen(Some((mi + n - 1) % n))), + Key::Named(NamedKey::ArrowRight) => Some(Msg::MenuOpen(Some((mi + 1) % n))), + Key::Named(NamedKey::ArrowDown) => Some(Msg::MenuNav(1)), + Key::Named(NamedKey::ArrowUp) => Some(Msg::MenuNav(-1)), + Key::Named(NamedKey::Enter) => Some(Msg::MenuActivate), + _ => None, + }; + } + if model.edit_menu.is_some() { + return match &event.key { + Key::Named(NamedKey::Escape) => Some(Msg::CloseMenus), + Key::Named(NamedKey::ArrowDown) => Some(Msg::EditNav(1)), + Key::Named(NamedKey::ArrowUp) => Some(Msg::EditNav(-1)), + Key::Named(NamedKey::Enter) => Some(Msg::EditActivate), + _ => None, + }; + } + } + // Con el prompt de passphrase abierto, las teclas son sólo suyas: + // Enter desbloquea, Esc cancela, el resto va al input. + if model.unlocking { + if event.state == KeyState::Pressed && !event.repeat { + if matches!(&event.key, Key::Named(NamedKey::Enter)) { + return Some(Msg::Unlock); + } + if matches!(&event.key, Key::Named(NamedKey::Escape)) { + return Some(Msg::CancelUnlock); + } + } + return Some(Msg::Key(event.clone())); + } + // En modo recibir con foco en la dirección: Enter jala, Esc cancela. + if model.receiving && model.focus == Focus::PeerAddr { + if event.state == KeyState::Pressed && !event.repeat { + if matches!(&event.key, Key::Named(NamedKey::Enter)) { + return Some(Msg::FetchManual); + } + if matches!(&event.key, Key::Named(NamedKey::Escape)) { + return Some(Msg::CancelPeers); + } + } + return Some(Msg::Key(event.clone())); + } + // Bautizando una región: Enter confirma, Esc cancela, resto al input. + if model.naming.is_some() && model.focus == Focus::Region { + if event.state == KeyState::Pressed && !event.repeat { + if matches!(&event.key, Key::Named(NamedKey::Enter)) { + return Some(Msg::CommitNaming); + } + if matches!(&event.key, Key::Named(NamedKey::Escape)) { + return Some(Msg::CancelNaming); + } + } + return Some(Msg::Key(event.clone())); + } + // Atajo global: Ctrl+N (sin foco en input necesario) crea + // nota. Esc libera el foco. Cualquier otra tecla la dispatcha + // como `Key` al input/editor focado. + if event.state == KeyState::Pressed && !event.repeat { + if event.modifiers.ctrl + && matches!(&event.key, Key::Character(s) if s.eq_ignore_ascii_case("n")) + { + return Some(Msg::NewNote); + } + if matches!(&event.key, Key::Named(NamedKey::Escape)) { + return Some(Msg::EscapeMap); + } + } + Some(Msg::Key(event.clone())) + } + + fn title() -> &'static str { + "khipu" + } + + fn app_id() -> Option<&'static str> { + Some("gioser.khipu") + } + + fn initial_size() -> (u32, u32) { + (1280, 760) + } +} + + + +/// Sincroniza inputs/editor → store/field + persiste si cambió algo. +fn commit_edits(model: &mut Model, h: &Handle) { + let Some(id) = model.selected else { + return; + }; + let mut changed = false; + let new_title = model.title.text(); + let new_body = model.body.text(); + let new_tags = parse_tags(&model.tags.text()); + let now = now_secs(); + if let Some(note) = model.store.get_mut(id) { + if note.title != new_title { + note.title = new_title; + note.updated_at = now; + changed = true; + } + if note.body != new_body { + note.body = new_body; + note.updated_at = now; + changed = true; + } + if note.tags != new_tags { + note.tags = new_tags; + note.updated_at = now; + changed = true; + } + } + if changed { + // El texto ya está en el store: persistimos de inmediato para no + // perderlo. El embedding viaja a un worker y persistirá de nuevo + // cuando llegue (`Msg::EmbeddingReady`). + persist(model); + schedule_embedding(model, id, h); + } +} + +// ===================================================================== +// Menú principal + menú de edición contextual +// ===================================================================== + +/// Devuelve el `EditorState` del campo focuseado (referencia inmutable) y +/// si está enmascarado (passphrase). Search/PeerAddr/Title/Tags son +/// `TextInputState` (su `.editor()`); Body es el `EditorState` directo. +/// Sin foco editable devuelve `None`. +fn focused_editor(model: &Model) -> (Option<&EditorState>, bool) { + match model.focus { + Focus::Body => (Some(&model.body), false), + Focus::Title => (Some(model.title.editor()), false), + Focus::Tags => (Some(model.tags.editor()), false), + Focus::Search => (Some(model.search.editor()), false), + Focus::PeerAddr => (Some(model.peer_input.editor()), false), + Focus::Region => (Some(model.region_input.editor()), false), + Focus::Passphrase => (Some(model.passphrase.editor()), model.passphrase.is_masked()), + Focus::None => (None, false), + } +} + +/// `EditFlags` del campo focuseado, para nav/ejecución por teclado del +/// menú de edición. Sin campo focuseado, flags vacíos (todo gris). +fn focused_edit_flags(model: &Model) -> EditFlags { + let (editor, masked) = focused_editor(model); + match editor { + Some(ed) => EditFlags::from_editor(ed, masked), + None => EditFlags::default(), + } +} + +/// Construye el menú principal de khipu reflejando el estado del campo +/// focuseado (ítems de Editar grises sin selección / historial). +fn app_menu(model: &Model) -> app_bus::AppMenu { + use app_bus::{AppMenu, Menu, MenuItem}; + let (editor, _masked) = focused_editor(model); + let has_sel = editor.map(|e| e.has_selection()).unwrap_or(false); + let can_undo = editor.map(|e| e.can_undo()).unwrap_or(false); + let can_redo = editor.map(|e| e.can_redo()).unwrap_or(false); + let has_field = editor.is_some(); + let has_sel_note = model.selected.is_some(); + + let mut undo = MenuItem::new("Deshacer", "edit.undo").shortcut("Ctrl+Z"); + if !can_undo { + undo = undo.disabled(); + } + let mut redo = MenuItem::new("Rehacer", "edit.redo").shortcut("Ctrl+Y"); + if !can_redo { + redo = redo.disabled(); + } + let mut cut = MenuItem::new("Cortar", "edit.cut").shortcut("Ctrl+X").separated(); + let mut copy = MenuItem::new("Copiar", "edit.copy").shortcut("Ctrl+C"); + if !has_sel { + cut = cut.disabled(); + copy = copy.disabled(); + } + let mut paste = MenuItem::new("Pegar", "edit.paste").shortcut("Ctrl+V"); + if !has_field { + paste = paste.disabled(); + } + let mut sel_all = MenuItem::new("Seleccionar todo", "edit.selectall") + .shortcut("Ctrl+A") + .separated(); + if !has_field { + sel_all = sel_all.disabled(); + } + + let mut delete_note = MenuItem::new("Borrar nota", "note.delete"); + if !has_sel_note { + delete_note = delete_note.disabled(); + } + let archive_label = if model.show_archive { + "Ocultar archivadas" + } else { + "Ver archivadas" + }; + + AppMenu::new() + .menu( + Menu::new("Archivo") + .item(MenuItem::new("Nueva nota", "note.new").shortcut("Ctrl+N")) + .item(delete_note) + .item(MenuItem::new(archive_label, "note.archive").separated()) + .item(MenuItem::new("Exportar sobre…", "share.export")) + .item(MenuItem::new("Importar sobre…", "share.import")), + ) + .menu( + Menu::new("Editar") + .item(undo) + .item(redo) + .item(cut) + .item(copy) + .item(paste) + .item(sel_all), + ) + .menu( + Menu::new("Compartir") + .item(MenuItem::new("Publicar (P2P)", "share.publish")) + .item(MenuItem::new("Recibir de un par…", "share.receive")), + ) + .menu( + Menu::new("Ayuda") + .item(MenuItem::new("Buscar (foco)", "view.search").shortcut("Ctrl+F")) + .item(MenuItem::new("Acerca de khipu", "help.about")), + ) +} + +/// Arma el `MenuBarSpec` compartido por `menubar_view` y `menubar_overlay`. +fn menubar_spec<'a>( + menu: &'a app_bus::AppMenu, + model: &Model, + theme: &'a Theme, +) -> MenuBarSpec<'a, Msg> { + let (w, h) = KhipuApp::initial_size(); + MenuBarSpec { + menu, + open: model.menu_open, + theme, + viewport: (w as f32, h as f32), + height: MENU_H, + on_open: std::sync::Arc::new(Msg::MenuOpen), + on_command: std::sync::Arc::new(|c: &str| Msg::MenuCommand(c.to_string())), + } +} + +/// Traduce el `command` del menú principal al `Msg` real y lo redespacha +/// por el `update`. Cierra el menú antes de actuar. +fn handle_menu_command(mut model: Model, command: String, h: &Handle) -> Model { + model.menu_open = None; + let target = match command.as_str() { + "note.new" => Some(Msg::NewNote), + "note.delete" => Some(Msg::DeleteSelected), + "note.archive" => Some(Msg::ToggleArchive), + "share.export" => Some(Msg::Export), + "share.import" => Some(Msg::Import), + "share.publish" => Some(Msg::Publish), + "share.receive" => Some(Msg::Receive), + "view.search" => Some(Msg::Focus(Focus::Search)), + "edit.undo" => Some(Msg::EditMenuAction(EditAction::Undo)), + "edit.redo" => Some(Msg::EditMenuAction(EditAction::Redo)), + "edit.cut" => Some(Msg::EditMenuAction(EditAction::Cut)), + "edit.copy" => Some(Msg::EditMenuAction(EditAction::Copy)), + "edit.paste" => Some(Msg::EditMenuAction(EditAction::Paste)), + "edit.selectall" => Some(Msg::EditMenuAction(EditAction::SelectAll)), + "help.about" => { + model.status = Some("khipu · cuaderno de notas P2P soberano".into()); + None + } + _ => None, + }; + match target { + Some(msg) => KhipuApp::update(model, msg, h), + None => model, + } +} + +/// Aplica una acción del menú de edición al editor del campo focuseado, +/// usando el portapapeles del sistema, y replica el bookkeeping que khipu +/// hace tras editar (commit al store + embedding si cambió un campo que +/// vive en la nota). Cierra el menú de edición. +fn apply_edit_menu_action(mut model: Model, action: EditAction, h: &Handle) -> Model { + model.edit_menu = None; + let focus = model.focus; + let clip = &mut model.clipboard; + let result = match focus { + Focus::Body => Some(editmenu::apply(&mut model.body, action, clip)), + Focus::Title => Some(editmenu::apply(model.title.editor_mut(), action, clip)), + Focus::Tags => Some(editmenu::apply(model.tags.editor_mut(), action, clip)), + Focus::Search => Some(editmenu::apply(model.search.editor_mut(), action, clip)), + Focus::PeerAddr => Some(editmenu::apply(model.peer_input.editor_mut(), action, clip)), + Focus::Region => Some(editmenu::apply(model.region_input.editor_mut(), action, clip)), + Focus::Passphrase => Some(editmenu::apply(model.passphrase.editor_mut(), action, clip)), + Focus::None => None, + }; + // Si la acción cambió un campo persistente de la nota (título, cuerpo o + // tags), corremos el mismo commit que las teclas. Search/PeerAddr/ + // Passphrase no tocan el store. + if let Some(r) = result { + if r.changed() && matches!(focus, Focus::Body | Focus::Title | Focus::Tags) { + commit_edits(&mut model, h); + } + } + model +} + +fn parse_tags(raw: &str) -> Vec { + raw.split(',') + .map(|t| t.trim().to_string()) + .filter(|t| !t.is_empty()) + .collect() +} + +fn select(model: &mut Model, id: NoteId) { + let Some(note) = model.store.get(id) else { + return; + }; + model.selected = Some(id); + model.title.set_text(note.title.clone()); + model.body = EditorState::default(); + model.body.set_text(¬e.body); + model.tags.set_text(note.tags.join(", ")); + model.focus = Focus::Body; +} + +/// Suelta la nota seleccionada y limpia los campos del editor. El editor +/// flotante desaparece y el mapa queda libre — el equivalente a alejarse +/// del nodo (precursor del zoom semántico). +fn deselect(model: &mut Model) { + model.selected = None; + model.title.set_text(String::new()); + model.body = EditorState::default(); + model.tags.set_text(String::new()); + model.focus = Focus::None; +} + +/// Pide el embedding de `id` en segundo plano. Asigna una secuencia +/// nueva, la marca como vigente, y dispara un worker (`Handle::spawn`) +/// que al terminar reentra al `update` con [`Msg::EmbeddingReady`]. Así +/// el `block_on` del arm remoto nunca corre en el hilo de UI. +fn schedule_embedding(model: &mut Model, id: NoteId, h: &Handle) { + let Some(note) = model.store.get(id) else { + return; + }; + let combined = format!("{} {}", note.title, note.body); + model.embed_seq += 1; + let seq = model.embed_seq; + model.embed_latest.insert(id, seq); + let embedder = model.embedder.clone(); + h.spawn(move || { + let v = embedder.embed_blocking(&combined); + Msg::EmbeddingReady(id, seq, v) + }); +} + +/// Versión síncrona para el arranque (seed y migración de formato): +/// calcula el vector en línea y lo inserta. En init todavía no hay nada +/// que repintar, así que bloquear un instante es lo correcto — y deja el +/// campo semántico listo antes del primer layout. +fn embed_now(model: &mut Model, id: NoteId) { + let Some(note) = model.store.get(id) else { + return; + }; + let combined = format!("{} {}", note.title, note.body); + let v = model.embedder.embed_blocking(&combined); + model.field.insert(id, v); +} + +/// Hash trigram → R^EMBED_DIM con signos +/-1 (random projection +/// 1-bit signed), normalizado por L2. Determinista, independiente de +/// idioma, sin red. +fn embed(text: &str, dim: usize) -> Vec { + let mut v = vec![0.0f32; dim]; + let lower = text.to_lowercase(); + let bytes = lower.as_bytes(); + if bytes.len() < 3 { + for (i, b) in bytes.iter().enumerate() { + v[i % dim] += *b as f32 / 255.0; + } + } else { + for w in bytes.windows(3) { + let mut h: u64 = 0xcbf29ce484222325; + for b in w { + h ^= *b as u64; + h = h.wrapping_mul(0x100000001b3); + } + let idx = (h as usize) % dim; + let sign = if h & 1 == 0 { 1.0 } else { -1.0 }; + v[idx] += sign; + } + } + let n: f32 = v.iter().map(|x| x * x).sum::().sqrt(); + if n > 0.0 { + for x in &mut v { + *x /= n; + } + } + v +} + +#[derive(Serialize, Deserialize)] +struct PersistedState { + store: NoteStore, + embeddings: Vec<(NoteId, Vec)>, + order: Vec, + /// Etiqueta del espacio vectorial con que se guardaron los + /// `embeddings` (ver [`Embedder::label`]). Si al cargar no coincide + /// con el embebedor activo, los vectores se recalculan. + model: String, + /// Topónimos bautizados. Trailing → archivos previos a las regiones + /// no parsean como esta forma y caen al fallback `PersistedStateV2`. + #[serde(default)] + regions: Vec, +} + +/// Formato previo a las regiones (postcard no es self-describing, así que +/// un campo trailing rompe el parseo y hay que intentar la forma vieja). +#[derive(Deserialize)] +struct PersistedStateV2 { + store: NoteStore, + embeddings: Vec<(NoteId, Vec)>, + order: Vec, + model: String, +} + +/// Formato histórico, sin `model`. Fallback cuando ni el actual ni el V2 +/// parsean (archivos escritos antes de enchufar `verbo`). +#[derive(Deserialize)] +struct PersistedStateV1 { + store: NoteStore, + embeddings: Vec<(NoteId, Vec)>, + order: Vec, +} + +/// Directorio de datos de khipu (`$XDG_DATA_HOME/khipu/`), creándolo si +/// hace falta. Raíz de `notes.bin`, `identidad.seed` y `compartido.khipu`. +fn khipu_dir() -> Option { + let dirs = ProjectDirs::from("org", "gioser", "khipu")?; + let dir = dirs.data_dir().to_path_buf(); + std::fs::create_dir_all(&dir).ok()?; + Some(dir) +} + +fn data_file_path() -> Option { + Some(khipu_dir()?.join("notes.bin")) +} + +/// Desbloquea (o crea, o migra) la identidad del cuaderno con `passphrase`, +/// vía [`khipu_share::identity::unlock`]. La semilla vive cifrada en +/// `/keys/`; si existe un `identidad.seed` en claro de versiones +/// viejas, se migra al keystore y se borra el claro. `None` si no hay +/// directorio de datos o la passphrase no descifra. +fn unlock_identity(passphrase: &str) -> Option { + let dir = khipu_dir()?; + let legacy = dir.join("identidad.seed"); + khipu_share::identity::unlock(&dir.join("keys"), Some(&legacy), passphrase).ok() +} + +/// Arranca el prompt de passphrase y memoriza la acción a reanudar. +fn start_unlock(model: &mut Model, accion: Msg) { + model.unlocking = true; + model.pending = Some(Box::new(accion)); + model.focus = Focus::Passphrase; + model.passphrase.clear(); + model.status = Some("ingresá tu passphrase para desbloquear la identidad".into()); +} + +/// Prompt modal de passphrase: tarjeta centrada con el input (enmascarado +/// con •) y dos botones. Enter desbloquea, Esc cancela (ver `on_key`). +fn unlock_view(model: &Model, input_palette: &TextInputPalette) -> View { + let titulo = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(22.0_f32), + }, + ..Default::default() + }) + .text_aligned( + "Desbloqueá tu identidad para firmar".to_string(), + 14.0, + model.theme.fg_text, + Alignment::Start, + ); + + let hint = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(30.0_f32), + }, + ..Default::default() + }) + .text_aligned( + "La semilla vive cifrada (Argon2id). La primera vez, esta passphrase la crea." + .to_string(), + 11.0, + model.theme.fg_muted, + Alignment::Start, + ); + + let input = text_input_view( + &model.passphrase, + "passphrase", + model.focus == Focus::Passphrase, + input_palette, + Msg::Focus(Focus::Passphrase), + ); + let input_row = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(30.0_f32), + }, + flex_shrink: 0.0, + ..Default::default() + }) + .children(vec![input]); + + let unlock_btn = button( + "desbloquear (Enter)", + model.theme.bg_button, + model.theme.accent, + Msg::Unlock, + ); + let cancel_btn = button( + "cancelar (Esc)", + model.theme.bg_button, + model.theme.fg_muted, + Msg::CancelUnlock, + ); + let buttons = View::new(Style { + flex_direction: FlexDirection::Row, + size: Size { + width: percent(1.0_f32), + height: length(30.0_f32), + }, + gap: Size { + width: length(8.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .children(vec![unlock_btn, cancel_btn]); + + let card = View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: length(420.0_f32), + height: Dimension::auto(), + }, + padding: Rect { + left: length(18.0_f32), + right: length(18.0_f32), + top: length(16.0_f32), + bottom: length(16.0_f32), + }, + gap: Size { + width: length(0.0_f32), + height: length(10.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel) + .radius(6.0) + .children(vec![titulo, hint, input_row, buttons]); + + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + align_items: Some(AlignItems::Center), + justify_content: Some(JustifyContent::Center), + ..Default::default() + }) + .fill(model.theme.bg_app) + .children(vec![card]) +} + +/// Barra de estado al pie: muestra el último mensaje de export/import. +fn status_bar(model: &Model) -> Option> { + let text = model.status.as_ref()?; + Some( + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(22.0_f32), + }, + flex_shrink: 0.0, + padding: Rect { + left: length(12.0_f32), + right: length(12.0_f32), + top: length(2.0_f32), + bottom: length(2.0_f32), + }, + align_items: Some(AlignItems::Center), + ..Default::default() + }) + .fill(model.theme.bg_panel_alt) + .text_aligned(text.clone(), 11.0, model.theme.fg_muted, Alignment::Start), + ) +} + +fn load_state(path: &PathBuf) -> Option { + let bytes = std::fs::read(path).ok()?; + // Formato actual primero; si no parsea (payload viejo sin `model`) + // caemos al V1 y lo migramos con `model` vacío → fuerza recálculo. + if let Ok(state) = postcard::from_bytes::(&bytes) { + return Some(state); + } + // Archivo previo a las regiones: misma forma sin el campo trailing. + if let Ok(v2) = postcard::from_bytes::(&bytes) { + return Some(PersistedState { + store: v2.store, + embeddings: v2.embeddings, + order: v2.order, + model: v2.model, + regions: Vec::new(), + }); + } + let v1: PersistedStateV1 = postcard::from_bytes(&bytes).ok()?; + Some(PersistedState { + store: v1.store, + embeddings: v1.embeddings, + order: v1.order, + model: String::new(), + regions: Vec::new(), + }) +} + +fn persist(model: &Model) { + let Some(path) = model.data_path.as_ref() else { + return; + }; + let state = PersistedState { + store: model.store.clone(), + embeddings: model + .field + .iter() + .map(|(id, v)| (id, v.to_vec())) + .collect(), + order: model.order.clone(), + model: model.embedder.label(), + regions: model.regions.clone(), + }; + if let Ok(bytes) = postcard::to_allocvec(&state) { + let tmp = path.with_extension("bin.tmp"); + if std::fs::write(&tmp, &bytes).is_ok() { + let _ = std::fs::rename(&tmp, path); + } + } +} + +fn from_state(state: PersistedState, embedder: Embedder) -> Model { + // ¿Los vectores guardados son del mismo espacio que el embebedor + // activo? Si cambió el modelo o la dimensión (p. ej. arrancó el + // daemon, o se cayó y volvimos al trigram local), son incomparables: + // se descartan y se recalcula todo el cuaderno. + let same_space = !state.model.is_empty() && state.model == embedder.label(); + let regions = state.regions; + let mut model = Model { + store: state.store, + field: SemanticField::new(), + order: state.order, + selected: None, + title: TextInputState::new(), + body: EditorState::default(), + tags: TextInputState::new(), + search: TextInputState::new(), + focus: Focus::None, + theme: Theme::dark(), + data_path: None, + gravity: Gravity::new(Params::default()), + show_archive: false, + embedder, + embed_latest: BTreeMap::new(), + embed_seq: 0, + keypair: None, + status: None, + publishing: false, + receiving: false, + peers: Vec::new(), + peer_input: TextInputState::new(), + passphrase: TextInputState::masked(), + unlocking: false, + pending: None, + p2p: None, + menu_open: None, + menu_active: usize::MAX, + menu_anim: Tween::idle(1.0), + edit_menu: None, + edit_active: usize::MAX, + edit_anim: Tween::idle(1.0), + clipboard: SystemClipboard::new(), + cam_pan: (0.0, 0.0), + cam_zoom: 1.0, + show_list: true, + canvas_size: (1280.0, 640.0), + regions, + naming: None, + region_input: TextInputState::new(), + }; + if same_space { + let restored: std::collections::HashSet = + state.embeddings.iter().map(|(id, _)| *id).collect(); + for (id, v) in &state.embeddings { + if !v.is_empty() { + model.field.insert(*id, v.clone()); + } + } + // Notas sin vector persistido (nota nueva que no alcanzó a + // guardar su embedding async): recalcular sólo esas. + let missing: Vec = model + .order + .iter() + .copied() + .filter(|id| !restored.contains(id)) + .collect(); + for id in missing { + embed_now(&mut model, id); + place_note(&mut model, id); + } + } else { + let ids: Vec = model.order.clone(); + for id in ids { + embed_now(&mut model, id); + } + } + // Notas cargadas de disco sin posición (payloads viejos previos al + // anclaje) reciben domicilio ahora, en orden, contra las ya asentadas. + let unplaced: Vec = model + .order + .iter() + .copied() + .filter(|id| model.store.get(*id).map(|n| n.pos.is_none()).unwrap_or(false)) + .collect(); + for id in unplaced { + place_note(&mut model, id); + } + model +} + +fn seeded_model(embedder: Embedder) -> Model { + let mut model = Model { + store: NoteStore::new(), + field: SemanticField::new(), + order: Vec::new(), + selected: None, + title: TextInputState::new(), + body: EditorState::default(), + tags: TextInputState::new(), + search: TextInputState::new(), + focus: Focus::None, + theme: Theme::dark(), + data_path: None, + gravity: Gravity::new(Params::default()), + show_archive: false, + embedder, + embed_latest: BTreeMap::new(), + embed_seq: 0, + keypair: None, + status: None, + publishing: false, + receiving: false, + peers: Vec::new(), + peer_input: TextInputState::new(), + passphrase: TextInputState::masked(), + unlocking: false, + pending: None, + p2p: None, + menu_open: None, + menu_active: usize::MAX, + menu_anim: Tween::idle(1.0), + edit_menu: None, + edit_active: usize::MAX, + edit_anim: Tween::idle(1.0), + clipboard: SystemClipboard::new(), + cam_pan: (0.0, 0.0), + cam_zoom: 1.0, + show_list: true, + canvas_size: (1280.0, 640.0), + regions: Vec::new(), + naming: None, + region_input: TextInputState::new(), + }; + let now = now_secs(); + let seed: [(&str, &str, &[&str]); 7] = [ + ( + "Índice", + "mi cuaderno: [[Recetas de la abuela]], [[Jardín]] y [[Oficina]]", + &["meta"], + ), + ( + "Recetas de la abuela", + "sopa de auyama; ver también [[Lista del mercado]]", + &["cocina"], + ), + ( + "Lista del mercado", + "auyama, cilantro, pan; vuelve al [[Índice]]", + &["cocina"], + ), + ( + "Jardín", + "riego semanal; las [[Semillas de cilantro]] van en marzo", + &["jardín"], + ), + ( + "Semillas de cilantro", + "germinan en diez días", + &["jardín"], + ), + ( + "Oficina", + "[[Reunión del lunes]] y pendientes varios", + &["trabajo"], + ), + ( + "Diario sin enlaces", + "una nota suelta, no la enlaza nadie", + &["personal"], + ), + ]; + for (title, body, tags) in seed { + let tags: Vec = tags.iter().map(|s| s.to_string()).collect(); + let id = model.store.create(title, body, tags, now); + model.order.push(id); + embed_now(&mut model, id); + place_note(&mut model, id); + } + model +} + +fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// La masa "vivida" de una nota en `now`: la guardada decae contra +/// el tiempo transcurrido desde `last_access`. Las notas con +/// `last_access == 0` (payloads viejos sin el campo) toman su `mass` +/// tal cual — equivale a tratar `now` como su primer acceso. +fn current_mass(gravity: &Gravity, n: &Note, now: u64) -> f32 { + if n.last_access == 0 { + return n.mass; + } + let dt = if now > n.last_access { + (now - n.last_access) as f32 + } else { + 0.0 + }; + gravity.decay(n.mass, dt) +} + +/// Refuerza la masa de `id` y marca `last_access`. El gesto canónico +/// cuando el usuario selecciona o abre una nota: primero decaemos el +/// valor guardado al "ahora" y sobre ese decaído sumamos el boost. +fn reinforce_and_touch(model: &mut Model, id: NoteId) { + let now = now_secs(); + let Some(n) = model.store.get(id) else { + return; + }; + let lived = current_mass(&model.gravity, n, now); + let reinforced = model.gravity.reinforce(lived); + model.store.set_mass(id, reinforced); + model.store.touch(id, now); +} + +/// Primera nota sobre el horizonte, ordenada por masa "viva". +fn first_visible(model: &Model) -> Option { + let now = now_secs(); + let mut visible: Vec<(NoteId, f32)> = model + .order + .iter() + .filter_map(|id| { + model.store.get(*id).and_then(|n| { + let m = current_mass(&model.gravity, n, now); + model.gravity.is_visible(m).then_some((*id, m)) + }) + }) + .collect(); + visible.sort_by(|a, b| { + b.1.partial_cmp(&a.1) + .unwrap_or(core::cmp::Ordering::Equal) + .then(a.0.cmp(&b.0)) + }); + visible.first().map(|(id, _)| *id) +} + +fn main() { + llimphi_ui::run::(); +} diff --git a/00_unanchay/khipu/khipu-app/src/map.rs b/00_unanchay/khipu/khipu-app/src/map.rs new file mode 100644 index 0000000..a378c83 --- /dev/null +++ b/00_unanchay/khipu/khipu-app/src/map.rs @@ -0,0 +1,655 @@ +//! `map` — el lienzo de pensamientos: geometría de cámara, colocación +//! determinista de notas (anclaje), detección de regiones emergentes y el +//! pintado del mapa (nodos que respiran por masa, filamentos, topónimos). +//! +//! El resto de la app le pide vistas y consultas; los helpers compartidos +//! (`current_mass`, `now_secs`, `button`, `CLUSTER_THRESHOLD`) viven en la +//! raíz y se alcanzan por `crate::`. + +use llimphi_theme::Theme; +use llimphi_ui::llimphi_layout::taffy::{ + prelude::{auto, length, percent, FlexDirection, Position, Rect, Size, Style}, + AlignItems, Dimension, JustifyContent, +}; +use llimphi_ui::llimphi_raster::kurbo::{Affine, BezPath, Circle as KurboCircle, Stroke}; +use llimphi_ui::llimphi_raster::peniko::{Color, Fill}; +use llimphi_ui::llimphi_text::{draw_block, Alignment, TextBlock}; +use llimphi_ui::{DragPhase, View}; +use llimphi_widget_text_input::{text_input_view, TextInputPalette}; +use khipu_core::NoteId; + +use crate::panels::button; +use crate::{current_mass, now_secs, Focus, Model, Msg, CLUSTER_THRESHOLD}; + +/// Un nodo del mapa, ya resuelto a coordenadas de mundo + su masa viva. +/// Datos planos para viajar dentro de la closure de pintura. +pub(crate) struct MapNode { + id: NoteId, + /// Coordenadas de mundo (el domicilio fijo de la nota). + x: f32, + y: f32, + /// Masa "vivida" en el instante del render: enciende el brillo y el + /// tamaño. Decae con el tiempo → el mapa respira sin que toques nada. + mass: f32, + /// `false` si cayó bajo el horizonte (sólo se ve con archivo activo). + visible: bool, + color: Color, + label: String, +} + +/// Mundo → pantalla local (relativa al rect del lienzo). El centro del +/// rect es el ancla del zoom; `pan` se suma en mundo, luego se escala. +pub(crate) fn world_to_local(wx: f32, wy: f32, w: f32, h: f32, pan: (f32, f32), zoom: f32) -> (f32, f32) { + (w * 0.5 + (wx + pan.0) * zoom, h * 0.5 + (wy + pan.1) * zoom) +} + +/// Inversa de [`world_to_local`]: pantalla local → mundo. Para resolver +/// qué nota cae bajo un click. +pub(crate) fn local_to_world(lx: f32, ly: f32, w: f32, h: f32, pan: (f32, f32), zoom: f32) -> (f32, f32) { + let z = zoom.max(1e-3); + ((lx - w * 0.5) / z - pan.0, (ly - h * 0.5) / z - pan.1) +} + +/// La nota colocada más cercana a un click en coords locales, dentro de un +/// radio de tolerancia (~18 px de pantalla). `None` si el click cae en el +/// vacío — así arrastrar el fondo no cambia la selección. +pub(crate) fn pick_note(model: &Model, lx: f32, ly: f32, w: f32, h: f32) -> Option { + let (wx, wy) = local_to_world(lx, ly, w, h, model.cam_pan, model.cam_zoom); + let now = now_secs(); + let mut best: Option<(NoteId, f32)> = None; + for id in &model.order { + let Some(n) = model.store.get(*id) else { continue }; + let Some((px, py)) = n.pos else { continue }; + if !model.show_archive { + let m = current_mass(&model.gravity, n, now); + if !model.gravity.is_visible(m) { + continue; + } + } + let d2 = (px - wx).powi(2) + (py - wy).powi(2); + if best.map(|(_, bd)| d2 < bd).unwrap_or(true) { + best = Some((*id, d2)); + } + } + let tol = (18.0 / model.cam_zoom.max(1e-3)).powi(2); + best.filter(|(_, d2)| *d2 <= tol).map(|(id, _)| id) +} + +/// Separación mínima entre nodos al colocarlos (coordenadas de mundo). +pub(crate) const MAP_MIN_SEP: f32 = 30.0; +/// Ángulo áureo en radianes — reparte determinísticamente lo que no tiene +/// parentela semántica sin amontonarlo. +pub(crate) const GOLDEN_ANGLE: f32 = 2.399_963_2; + +/// Le da a `id` un domicilio fijo en el mapa, **una sola vez**: cae en el +/// baricentro de sus parientes semánticos (ponderado por afinidad) y, si +/// quedó pegada a otra nota, se separa apenas. Determinista y dependiente +/// sólo de las notas ya asentadas, así el orden de inserción es estable y +/// el mapa nunca se reacomoda solo. +pub(crate) fn place_note(model: &mut Model, id: NoteId) { + if model.store.get(id).map(|n| n.pos.is_some()).unwrap_or(true) { + return; // ya tiene domicilio (o no existe): no se mueve. + } + // Vecinos ya colocados: su afinidad con la nota nueva y su posición. + let mut kin: Vec<(f32, (f32, f32))> = Vec::new(); + for other in &model.order { + if *other == id { + continue; + } + let Some(pos) = model.store.get(*other).and_then(|n| n.pos) else { continue }; + let aff = model.field.affinity(id, *other).unwrap_or(0.0).max(0.0); + kin.push((aff, pos)); + } + + let target = if kin.is_empty() { + (0.0, 0.0) // primera nota del cuaderno: centro del mundo. + } else { + let wsum: f32 = kin.iter().map(|(w, _)| *w).sum(); + if wsum > 1e-3 { + // Cae junto a su parentela: baricentro ponderado por afinidad. + let (mut tx, mut ty) = (0.0_f32, 0.0_f32); + for (w, (x, y)) in &kin { + tx += w * x; + ty += w * y; + } + (tx / wsum, ty / wsum) + } else { + // Ortogonal a todo: anillo determinista por id, lejos del núcleo. + let ang = id as f32 * GOLDEN_ANGLE; + let rad = 180.0 + 14.0 * (id as f32).sqrt(); + (rad * ang.cos(), rad * ang.sin()) + } + }; + + // Separación: empuja el target hasta despegarlo de cada vecino cercano. + let mut p = target; + for _ in 0..12 { + let mut moved = false; + for (_, q) in &kin { + let dx = p.0 - q.0; + let dy = p.1 - q.1; + let d = (dx * dx + dy * dy).sqrt(); + if d < MAP_MIN_SEP { + let (ux, uy) = if d > 1e-3 { + (dx / d, dy / d) + } else { + let a = id as f32 * GOLDEN_ANGLE; + (a.cos(), a.sin()) + }; + let push = MAP_MIN_SEP - d; + p.0 += ux * push; + p.1 += uy * push; + moved = true; + } + } + if !moved { + break; + } + } + + model.store.set_pos(id, p.0, p.1); +} + +/// Envuelve `child` como cajón absoluto pegado al borde izquierdo, alto +/// completo con un margen. El mapa de fondo sigue recibiendo pan/zoom en +/// el resto de la ventana; sólo los clicks sobre el cajón los come él. +pub(crate) fn overlay_left(child: View, width: f32) -> View { + View::new(Style { + position: Position::Absolute, + inset: Rect { + left: length(8.0_f32), + top: length(8.0_f32), + bottom: length(8.0_f32), + right: auto(), + }, + size: Size { + width: length(width), + height: auto(), + }, + flex_direction: FlexDirection::Column, + ..Default::default() + }) + .children(vec![child]) +} + +/// Columna interna del editor: barra de cierre (× ⇒ deselecciona) arriba + +/// el editor abajo, sobre `bg_panel`. La comparten el overlay lateral y la +/// tarjeta anclada del zoom semántico. +pub(crate) fn editor_shell(child: View, theme: &Theme) -> View { + let close = button("× cerrar", theme.bg_button, theme.fg_muted, Msg::Deselect); + let close_row = View::new(Style { + flex_direction: FlexDirection::Row, + size: Size { + width: percent(1.0_f32), + height: length(28.0_f32), + }, + flex_shrink: 0.0, + justify_content: Some(JustifyContent::End), + align_items: Some(AlignItems::Center), + padding: Rect { + left: length(6.0_f32), + right: length(6.0_f32), + top: length(2.0_f32), + bottom: length(2.0_f32), + }, + ..Default::default() + }) + .fill(theme.bg_panel_alt) + .children(vec![close]); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + ..Default::default() + }) + .fill(theme.bg_panel) + .children(vec![close_row, child]) +} + +/// Envuelve `child` como panel absoluto pegado al borde derecho, alto +/// completo, con barra de cierre. El editor del nodo abierto cuando se lo +/// edita de lejos (zoom bajo): un fallback práctico al anclaje in-situ. +pub(crate) fn overlay_right(child: View, width: f32, theme: &Theme) -> View { + View::new(Style { + position: Position::Absolute, + inset: Rect { + left: auto(), + top: length(8.0_f32), + bottom: length(8.0_f32), + right: length(8.0_f32), + }, + size: Size { + width: length(width), + height: auto(), + }, + flex_direction: FlexDirection::Column, + ..Default::default() + }) + .children(vec![editor_shell(child, theme)]) +} + +/// Mundo → pantalla local usando el último tamaño de lienzo conocido + la +/// cámara. La versión de `view()`, donde el rect real aún no se sabe. +pub(crate) fn world_screen(model: &Model, wx: f32, wy: f32) -> (f32, f32) { + let (w, h) = model.canvas_size; + world_to_local(wx, wy, w, h, model.cam_pan, model.cam_zoom) +} + +/// Posición de pantalla (local al lienzo) del nodo `id`. `None` si la nota +/// no tiene domicilio todavía. +pub(crate) fn node_screen_pos(model: &Model, id: NoteId) -> Option<(f32, f32)> { + let (wx, wy) = model.store.get(id).and_then(|n| n.pos)?; + Some(world_screen(model, wx, wy)) +} + +/// Mínimo de notas para que un clúster cuente como región candidata. +pub(crate) const REGION_MIN_MEMBERS: usize = 3; +/// Distancia de mundo dentro de la cual una región ya "posee" un clúster: +/// si hay un topónimo así de cerca del centroide, no se vuelve a ofrecer. +pub(crate) const REGION_MATCH_DIST: f32 = 140.0; + +/// Centroides (mundo) de los clústeres densos que todavía no tienen una +/// región cerca — los lugares que el mapa ofrece bautizar. Sólo cuentan +/// miembros colocados y visibles. +pub(crate) fn unnamed_cluster_centroids(model: &Model) -> Vec<(f32, f32)> { + let now = now_secs(); + let mut out = Vec::new(); + for cluster in model.field.clusters(CLUSTER_THRESHOLD) { + let pts: Vec<(f32, f32)> = cluster + .iter() + .filter_map(|id| { + let n = model.store.get(*id)?; + let p = n.pos?; + let m = current_mass(&model.gravity, n, now); + (model.show_archive || model.gravity.is_visible(m)).then_some(p) + }) + .collect(); + if pts.len() < REGION_MIN_MEMBERS { + continue; + } + let (sx, sy) = pts.iter().fold((0.0, 0.0), |(ax, ay), (x, y)| (ax + x, ay + y)); + let c = (sx / pts.len() as f32, sy / pts.len() as f32); + let d2 = REGION_MATCH_DIST * REGION_MATCH_DIST; + let near_named = model + .regions + .iter() + .any(|r| (r.x - c.0).powi(2) + (r.y - c.1).powi(2) <= d2); + let naming_here = model + .naming + .map(|(nx, ny)| (nx - c.0).powi(2) + (ny - c.1).powi(2) <= d2) + .unwrap_or(false); + if !near_named && !naming_here { + out.push(c); + } + } + out +} + +/// Chip clickeable "✛ nombrar zona" que ofrece bautizar el clúster denso +/// en `(wx, wy)`. Al click abre el input de bautizo en esa coordenada. +pub(crate) fn name_region_chip(wx: f32, wy: f32, theme: &Theme) -> View { + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + align_items: Some(AlignItems::Center), + justify_content: Some(JustifyContent::Center), + ..Default::default() + }) + .fill(theme.bg_button) + .radius(12.0) + .hover_fill(theme.bg_button_hover) + .text_aligned("✛ nombrar zona", 11.0, theme.fg_muted, Alignment::Center) + .on_click(Msg::BeginNaming(wx, wy)) +} + +/// Mini-input del bautizo en curso: una tarjeta con el campo de texto +/// enfocado. Enter confirma, Esc cancela (en `on_key`). +pub(crate) fn naming_input(model: &Model, input_palette: &TextInputPalette) -> View { + let input = text_input_view( + &model.region_input, + "nombre de la zona…", + model.focus == Focus::Region, + input_palette, + Msg::Focus(Focus::Region), + ); + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + align_items: Some(AlignItems::Center), + padding: Rect { + left: length(6.0_f32), + right: length(6.0_f32), + top: length(4.0_f32), + bottom: length(4.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel) + .radius(8.0) + .children(vec![input]) +} + +/// Posiciona `child` como vista absoluta de tamaño `(w, h)` centrada en la +/// pantalla `(sx, sy)`, clampeada al lienzo. Para chips y mini-inputs que +/// viven en el mapa (sugerencia de bautizo, input de nombre). +pub(crate) fn pinned(child: View, sx: f32, sy: f32, w: f32, h: f32, canvas: (f32, f32)) -> View { + let left = (sx - w * 0.5).clamp(4.0, (canvas.0 - w - 4.0).max(4.0)); + let top = (sy - h * 0.5).clamp(4.0, (canvas.1 - h - 4.0).max(4.0)); + View::new(Style { + position: Position::Absolute, + inset: Rect { + left: length(left), + top: length(top), + right: auto(), + bottom: auto(), + }, + size: Size { + width: length(w), + height: length(h), + }, + ..Default::default() + }) + .children(vec![child]) +} + +/// La tarjeta del nodo abierto, anclada a su coordenada `(nx, ny)` de +/// pantalla: el zoom semántico hecho carne — el editor vive EN el lugar del +/// pensamiento, no en un panel aparte. Se clampea para no salirse del +/// lienzo. Hija del canvas, así pan/zoom la arrastran con el nodo. +pub(crate) fn node_card(child: View, nx: f32, ny: f32, canvas: (f32, f32), theme: &Theme) -> View { + let (cw_max, ch_max) = canvas; + let cw = 380.0_f32.min((cw_max - 16.0).max(220.0)); + let ch = 440.0_f32.min((ch_max - 16.0).max(200.0)); + // Anclada bajo el nodo, centrada en X, clampeada a la ventana. + let left = (nx - cw * 0.5).clamp(8.0, (cw_max - cw - 8.0).max(8.0)); + let top = (ny + 16.0).clamp(8.0, (ch_max - ch - 8.0).max(8.0)); + + View::new(Style { + position: Position::Absolute, + inset: Rect { + left: length(left), + top: length(top), + right: auto(), + bottom: auto(), + }, + size: Size { + width: length(cw), + height: length(ch), + }, + flex_direction: FlexDirection::Column, + ..Default::default() + }) + .radius(8.0) + .children(vec![editor_shell(child, theme)]) +} + +pub(crate) fn gravity_panel(model: &Model, injected: Vec>) -> View { + let theme = model.theme; + let now = now_secs(); + let clusters = model.field.clusters(CLUSTER_THRESHOLD); + let selected = model.selected; + let pan = model.cam_pan; + let zoom = model.cam_zoom; + + // Nodos colocados (los que ya tienen domicilio), con su masa viva. + let mut nodes: Vec = Vec::new(); + for id in &model.order { + let Some(n) = model.store.get(*id) else { continue }; + let Some((x, y)) = n.pos else { continue }; + let mass = current_mass(&model.gravity, n, now); + let visible = model.gravity.is_visible(mass); + if !visible && !model.show_archive { + continue; + } + nodes.push(MapNode { + id: *id, + x, + y, + mass, + visible, + color: cluster_color(*id, &clusters, theme), + label: short_label(&n.title), + }); + } + + // Topónimos: las regiones bautizadas, para pintarlas como rótulos de + // continente detrás de los nodos. + let regions: Vec<(String, f32, f32)> = model + .regions + .iter() + .map(|r| (r.name.clone(), r.x, r.y)) + .collect(); + + // Filamentos del nodo seleccionado: sus parientes más afines ya + // colocados. Elegir un pensamiento enciende sus vecinos (activación + // por difusión) — el motor de serendipia. + let mut links: Vec<((f32, f32), (f32, f32), f32)> = Vec::new(); + if let Some(sel) = selected { + if let Some(sp) = model.store.get(sel).and_then(|n| n.pos) { + for (nid, aff) in model.field.nearest(sel, 6) { + if aff < 0.20 { + continue; + } + if let Some(np) = model.store.get(nid).and_then(|n| n.pos) { + links.push((sp, np, aff)); + } + } + } + } + + let canvas = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + ..Default::default() + }) + .fill(theme.bg_panel_alt) + .paint_with(move |scene, ts, rect| { + paint_map(scene, ts, rect, &nodes, &links, ®ions, selected, pan, zoom, theme); + }) + .draggable_at(|phase, dx, dy, _lx, _ly| match phase { + DragPhase::Move => Some(Msg::MapPan(dx, dy)), + DragPhase::End => None, + }) + .on_scroll(|_dx, dy| Some(Msg::MapZoom(dy))) + .on_click_at(|lx, ly, w, h| Some(Msg::MapClick(lx, ly, w, h))) + // La tarjeta del nodo abierto (zoom semántico) viaja como hija del + // canvas: se pinta encima de los nodos y la cámara la arrastra con el + // pensamiento al que pertenece. + .children(injected); + + View::new(Style { + size: Size { + width: Dimension::auto(), + height: percent(1.0_f32), + }, + flex_grow: 1.0, + flex_basis: length(0.0_f32), + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + padding: Rect { + left: length(4.0_f32), + right: length(4.0_f32), + top: length(4.0_f32), + bottom: length(4.0_f32), + }, + ..Default::default() + }) + .fill(theme.bg_panel) + .children(vec![canvas]) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn paint_map( + scene: &mut llimphi_ui::llimphi_raster::vello::Scene, + ts: &mut llimphi_ui::llimphi_text::Typesetter, + rect: llimphi_ui::PaintRect, + nodes: &[MapNode], + links: &[((f32, f32), (f32, f32), f32)], + regions: &[(String, f32, f32)], + selected: Option, + pan: (f32, f32), + zoom: f32, + theme: Theme, +) { + if rect.w <= 0.0 || rect.h <= 0.0 { + return; + } + // Pantalla absoluta = origen del rect + pantalla local. + let to_screen = |wx: f32, wy: f32| -> (f64, f64) { + let (lx, ly) = world_to_local(wx, wy, rect.w, rect.h, pan, zoom); + ((rect.x + lx) as f64, (rect.y + ly) as f64) + }; + + // Topónimos al fondo: el nombre de cada región, grande y tenue, como + // rótulo de continente; un halo suave insinúa su territorio. + for (name, rx, ry) in regions { + let (cx, cy) = to_screen(*rx, *ry); + let blob = KurboCircle::new((cx, cy), (96.0 * zoom as f64).max(34.0)); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + with_alpha(theme.accent, 0.05), + None, + &blob, + ); + let size = (15.0 * zoom).clamp(11.0, 28.0); + // Centrado aproximado: `simple` alinea a la izquierda en (x, y). + let est_w = name.chars().count() as f64 * size as f64 * 0.52; + draw_block( + scene, + ts, + &TextBlock::simple( + name, + size, + with_alpha(theme.fg_text, 0.30), + (cx - est_w * 0.5, cy - size as f64 * 0.6), + ), + ); + } + + // Filamentos primero (debajo de los nodos). Más opacos cuanto más afín. + for (a, b, aff) in links { + let (ax, ay) = to_screen(a.0, a.1); + let (bx, by) = to_screen(b.0, b.1); + let mut path = BezPath::new(); + path.move_to((ax, ay)); + path.line_to((bx, by)); + let alpha = (0.18 + aff * 0.55).clamp(0.0, 0.85); + scene.stroke( + &Stroke::new((0.8 + *aff as f64 * 1.6).max(0.6)), + Affine::IDENTITY, + with_alpha(theme.accent, alpha), + None, + &path, + ); + } + + // Nodos: tamaño y brillo crecen con la masa viva (el mapa respira). + for n in nodes { + let (px, py) = to_screen(n.x, n.y); + let m = n.mass.clamp(0.0, 2.0); + // Radio base por masa, escalado apenas por zoom para no inflarse. + let r = (3.0 + m * 4.5) * (0.6 + 0.4 * zoom.clamp(0.5, 1.5)); + // Brillo: las notas frescas arden; las que se enfrían se apagan + // hacia el fondo. Bajo el horizonte (archivo) van casi transparentes. + let glow = if n.visible { + (0.35 + m * 0.45).clamp(0.0, 1.0) + } else { + 0.18 + }; + let color = with_alpha(n.color, glow); + // Halo tenue alrededor de las notas más encendidas. + if n.visible && m > 0.6 { + let halo = KurboCircle::new((px, py), (r + 5.0) as f64); + scene.fill(Fill::NonZero, Affine::IDENTITY, with_alpha(n.color, 0.10), None, &halo); + } + let circle = KurboCircle::new((px, py), r as f64); + scene.fill(Fill::NonZero, Affine::IDENTITY, color, None, &circle); + + if selected == Some(n.id) { + let ring = KurboCircle::new((px, py), (r + 3.0) as f64); + scene.stroke(&Stroke::new(2.0), Affine::IDENTITY, theme.accent, None, &ring); + } + + // Etiqueta: sólo si el zoom da espacio o es la seleccionada — para + // no saturar el mapa lejano. El texto sale del Typesetter. + if (zoom >= 0.9 || selected == Some(n.id)) && n.visible { + let lbl_col = with_alpha(theme.fg_text, (glow + 0.25).clamp(0.0, 1.0)); + draw_block( + scene, + ts, + &TextBlock::simple(&n.label, 10.0, lbl_col, (px + r as f64 + 4.0, py - 7.0)), + ); + } + } +} + +pub(crate) fn cluster_color(id: NoteId, clusters: &[Vec], theme: Theme) -> Color { + let idx = clusters.iter().position(|c| c.contains(&id)).unwrap_or(0); + // Paleta tomada del theme + matices generados por golden-ratio + // sobre el hue del accent. Determinista por índice. + let palette: [Color; 6] = [ + theme.accent, + with_alpha(rotate_hue(theme.accent, 0.16), 1.0), + with_alpha(rotate_hue(theme.accent, 0.33), 1.0), + with_alpha(rotate_hue(theme.accent, 0.50), 1.0), + with_alpha(rotate_hue(theme.accent, 0.66), 1.0), + with_alpha(rotate_hue(theme.accent, 0.83), 1.0), + ]; + palette[idx % palette.len()] +} + +pub(crate) fn with_alpha(c: Color, alpha: f32) -> Color { + let [r, g, b, _] = c.components; + Color::new([r, g, b, alpha]) +} + +pub(crate) fn rotate_hue(c: Color, dh: f32) -> Color { + // RGB → HSV → rota H → RGB. Aproximación, alpha fijo. + let [r, g, b, a] = c.components; + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let v = max; + let s = if max <= 0.0 { 0.0 } else { (max - min) / max }; + let h = if (max - min).abs() < 1e-6 { + 0.0 + } else if max == r { + ((g - b) / (max - min)) % 6.0 + } else if max == g { + (b - r) / (max - min) + 2.0 + } else { + (r - g) / (max - min) + 4.0 + }; + let h2 = ((h / 6.0) + dh).rem_euclid(1.0) * 6.0; + let c2 = v * s; + let x = c2 * (1.0 - ((h2 % 2.0) - 1.0).abs()); + let (r2, g2, b2) = match h2 as i32 { + 0 => (c2, x, 0.0), + 1 => (x, c2, 0.0), + 2 => (0.0, c2, x), + 3 => (0.0, x, c2), + 4 => (x, 0.0, c2), + _ => (c2, 0.0, x), + }; + let m = v - c2; + Color::new([r2 + m, g2 + m, b2 + m, a]) +} + +pub(crate) fn short_label(s: &str) -> String { + let mut out: String = s.chars().take(24).collect(); + if s.chars().count() > 24 { + out.push('…'); + } + out +} diff --git a/00_unanchay/khipu/khipu-app/src/net.rs b/00_unanchay/khipu/khipu-app/src/net.rs new file mode 100644 index 0000000..6213c1c --- /dev/null +++ b/00_unanchay/khipu/khipu-app/src/net.rs @@ -0,0 +1,226 @@ +//! `net` — el lado P2P del cuaderno: sellar/exportar el sobre firmado, +//! verificar e ingerir uno ajeno, descubrir pares y publicarse por libp2p. +//! El transporte (LAN directo + WAN vía `card-net`/Kademlia + relay/NAT) +//! vive en `khipu-share`/`khipu-brahman`; acá sólo se orquesta. + +use std::sync::Arc; + +use khipu_share::{SharedNote, SignedBundle}; +use llimphi_ui::Handle; + +use crate::panels::note_matches; +use crate::{khipu_dir, now_secs, schedule_embedding, Model, Msg, P2p}; + +pub(crate) fn export_notebook(model: &Model) -> String { + let Some(kp) = model.keypair.as_ref() else { + return "sin identidad para firmar".into(); + }; + let Some(dir) = khipu_dir() else { + return "sin directorio de datos".into(); + }; + // Compartir selectivo: si hay texto en el buscador, exportamos sólo + // las notas que filtra (mismo criterio que la lista); si está vacío, + // todo el cuaderno. + let query = model.search.text(); + let q = query.trim(); + let notes: Vec = model + .order + .iter() + .filter_map(|id| model.store.get(*id)) + .filter(|n| q.is_empty() || note_matches(n, q)) + .map(SharedNote::from_note) + .collect(); + if notes.is_empty() { + return "no hay notas para exportar (¿el filtro no coincide?)".into(); + } + let n = notes.len(); + let sobre = match khipu_share::seal(kp, notes, now_secs()) { + Ok(s) => s, + Err(_) => return "falló el sellado".into(), + }; + let Ok(bytes) = sobre.to_bytes() else { + return "falló serializar el sobre".into(); + }; + let path = dir.join("compartido.khipu"); + let tmp = path.with_extension("khipu.tmp"); + if std::fs::write(&tmp, &bytes) + .and_then(|_| std::fs::rename(&tmp, &path)) + .is_err() + { + return "no se pudo escribir el sobre".into(); + } + let hash = sobre.content_address().unwrap_or([0u8; 32]); + let filtro = if q.is_empty() { + String::new() + } else { + format!(" (filtro «{q}»)") + }; + format!( + "exportadas {n} notas{filtro} → compartido.khipu · {}", + hex8(&hash) + ) +} + +/// Verifica e ingiere `compartido.khipu`. Las notas nuevas nacen con +/// gravedad fresca; sus embeddings se recalculan en segundo plano. Un +/// sobre con firma inválida se rechaza entero. Devuelve la línea de estado. +pub(crate) fn import_notebook(model: &mut Model, h: &Handle) -> String { + let Some(dir) = khipu_dir() else { + return "sin directorio de datos".into(); + }; + let path = dir.join("compartido.khipu"); + let Ok(bytes) = std::fs::read(&path) else { + return "no hay compartido.khipu para importar".into(); + }; + let sobre = match SignedBundle::from_bytes(&bytes) { + Ok(s) => s, + Err(_) => return "sobre ilegible".into(), + }; + let outcome = match khipu_share::open(&sobre) { + Ok(bundle) => khipu_share::import_into(&mut model.store, bundle, now_secs()), + Err(_) => return "firma inválida — sobre rechazado".into(), + }; + for id in &outcome.created { + model.order.push(*id); + schedule_embedding(model, *id, h); + } + format!( + "importadas {} · omitidas {} (ya existían)", + outcome.created.len(), + outcome.skipped + ) +} + +/// Dirección donde el servidor escucha. `KHIPU_BIND` la sobrescribe; +/// default localhost para no exponerse sin querer. +pub(crate) fn bind_addr() -> String { + std::env::var("KHIPU_BIND").unwrap_or_else(|_| "127.0.0.1:7700".into()) +} + +/// Dirección del par a quien jalarle el cuaderno. `KHIPU_PEER` la +/// sobrescribe; default coincide con [`bind_addr`] para probar en local. +pub(crate) fn peer_addr() -> String { + std::env::var("KHIPU_PEER").unwrap_or_else(|_| "127.0.0.1:7700".into()) +} + +/// Arma el nodo libp2p la primera vez que se necesita: runtime tokio +/// dedicado + `KhipuNode` que empieza a escuchar (para ser alcanzable y +/// obtener nuestra dirección de marcado). Idempotente. `false` si no se +/// pudo (sin runtime o sin red). +pub(crate) fn ensure_p2p(model: &mut Model) -> bool { + if model.p2p.is_some() { + return true; + } + let Ok(rt) = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + else { + return false; + }; + // `KhipuNode::standalone` arranca el swarm con `tokio::spawn`: hay que + // estar dentro del runtime. + let node = { + let _g = rt.enter(); + match khipu_brahman::KhipuNode::standalone() { + Ok(n) => Arc::new(n), + Err(_) => return false, + } + }; + let dial_addr = rt + .block_on(node.listen_str("/ip4/0.0.0.0/tcp/0")) + .unwrap_or_default(); + // Si hay un nodo bootstrap configurado, nos unimos a la malla DHT para + // poder descubrir y ser descubiertos (`anunciar`/`descubrir`). + if let Ok(boot) = std::env::var("KHIPU_BOOTSTRAP") { + let _ = node.dial_str(&boot); + } + model.p2p = Some(P2p { + rt: Arc::new(rt), + node, + dial_addr, + serving: false, + }); + true +} + +/// Levanta (una sola vez) el servidor TCP que sirve `compartido.khipu`. +/// El hilo lee el archivo en cada conexión, así sirve siempre la versión +/// vigente; vive hasta que el proceso termina. Devuelve la línea de estado. +pub(crate) fn start_publishing(model: &mut Model, h: &Handle) -> String { + if model.publishing { + return format!("ya publicando en {}", bind_addr()); + } + let Some(dir) = khipu_dir() else { + return "sin directorio de datos".into(); + }; + let addr = bind_addr(); + let listener = match std::net::TcpListener::bind(&addr) { + Ok(l) => l, + Err(e) => return format!("no se pudo escuchar en {addr}: {e}"), + }; + // Puerto efectivo (resuelve `:0` si se usara) para anunciarlo en la baliza. + let tcp_port = listener.local_addr().map(|a| a.port()).unwrap_or(0); + let path = dir.join("compartido.khipu"); + std::thread::spawn(move || { + khipu_share::net::serve_loop(listener, move || std::fs::read(&path)); + }); + // Baliza periódica para que los pares nos descubran sin saber la IP. + let beacon = khipu_share::discovery::Beacon { + author: model.keypair.as_ref().map(|k| k.public_key()).unwrap_or([0u8; 32]), + port: tcp_port, + name: "khipu".into(), + }; + std::thread::spawn(move || loop { + let _ = khipu_share::discovery::anunciar(&beacon); + std::thread::sleep(std::time::Duration::from_secs(2)); + }); + model.publishing = true; + + // Además del TCP/LAN, servimos por libp2p (cifrado, WAN). El nodo se + // arma perezoso; servimos `compartido.khipu` y nos anunciamos en la DHT. + let p2p_status = if ensure_p2p(model) { + let dir2 = dir.clone(); + if let Some(p) = model.p2p.as_mut() { + if !p.serving { + let path2 = dir2.join("compartido.khipu"); + let node = p.node.clone(); + let _g = p.rt.enter(); + node.run_serve(move || std::fs::read(&path2).ok()); + node.anunciar(); + p.serving = true; + } + // Si hay un relay configurado (KHIPU_RELAY=/ip4/.../p2p/), + // reservamos un circuito ahí para ser alcanzables detrás de NAT. + // Async (dial + identify + reserva tardan ~2s): cuando termina, + // reentra con Msg::RelayReady para mostrar la dirección. + if let Ok(relay) = std::env::var("KHIPU_RELAY") { + let (rt, node, h2) = (p.rt.clone(), p.node.clone(), h.clone()); + rt.spawn(async move { + let _ = node.dial_str(&relay); + // Esperamos a que AutoNAT confirme la dirección del relay + // (boot_delay + dial-back) antes de pedir la reserva. + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + let circuit = format!("{relay}/p2p-circuit"); + let msg = match node.listen_str(&circuit).await { + Ok(addr) => addr, + Err(e) => format!("falló reservar circuito: {e}"), + }; + h2.dispatch(Msg::RelayReady(msg)); + }); + } + format!(" · libp2p: {}", p.dial_addr) + } else { + String::new() + } + } else { + String::new() + }; + format!("publicando en {addr} (LAN){p2p_status}") +} + +/// Prefijo hex (4 bytes / 8 hex) de un hash, para mostrar una dirección +/// de contenido sin abrumar. +pub(crate) fn hex8(hash: &[u8; 32]) -> String { + hash[..4].iter().map(|b| format!("{b:02x}")).collect() +} diff --git a/00_unanchay/khipu/khipu-app/src/panels.rs b/00_unanchay/khipu/khipu-app/src/panels.rs new file mode 100644 index 0000000..9713866 --- /dev/null +++ b/00_unanchay/khipu/khipu-app/src/panels.rs @@ -0,0 +1,728 @@ +//! `panels` — los constructores de vistas en flujo: cabecera, cajón de +//! notas (lista + búsqueda), panel de recibir (pares P2P), y el editor de +//! la nota (título/cuerpo/etiquetas + stats). Frontends puros sobre el +//! `Model`; la lógica vive en la raíz y en `map`. + +use llimphi_ui::llimphi_layout::taffy::{ + prelude::{length, percent, FlexDirection, Rect, Size, Style}, + AlignItems, Dimension, JustifyContent, +}; +use llimphi_ui::llimphi_raster::peniko::Color; +use llimphi_ui::llimphi_text::Alignment; +use llimphi_ui::View; +use llimphi_widget_list::{list_view, ListPalette, ListRow, ListSpec}; +use llimphi_widget_text_editor::{text_editor_view, EditorMetrics, EditorPalette}; +use llimphi_widget_text_input::{text_input_view, TextInputPalette}; +use khipu_core::{Note, NoteId}; + +use crate::{ + current_mass, now_secs, Focus, Model, Msg, EDITOR_VISIBLE_LINES, FIELD_LABEL_SIZE, HEADER_H, + LIST_WIDTH, ROW_H, +}; + +pub(crate) fn header_view(model: &Model) -> View { + let title = format!("khipu · {} notas", model.store.len()); + let title_node = View::new(Style { + size: Size { + width: Dimension::auto(), + height: percent(1.0_f32), + }, + flex_grow: 1.0, + padding: Rect { + left: length(14.0_f32), + right: length(8.0_f32), + top: length(0.0_f32), + bottom: length(0.0_f32), + }, + align_items: Some(AlignItems::Center), + ..Default::default() + }) + .text_aligned(title, 14.0, model.theme.fg_text, Alignment::Start); + + let list_label = if model.show_list { "ocultar notas" } else { "☰ notas" }; + let list_btn = button( + list_label, + model.theme.bg_button, + if model.show_list { model.theme.accent } else { model.theme.fg_muted }, + Msg::ToggleList, + ); + let new_btn = button( + "+ nueva (Ctrl+N)", + model.theme.bg_button, + model.theme.fg_text, + Msg::NewNote, + ); + let archive_label = if model.show_archive { + "ocultar archivo" + } else { + "ver archivo" + }; + let archive_btn = button( + archive_label, + model.theme.bg_button, + model.theme.fg_muted, + Msg::ToggleArchive, + ); + let del_btn = button( + "borrar", + model.theme.bg_button, + model.theme.fg_muted, + Msg::DeleteSelected, + ); + let export_btn = button( + "exportar", + model.theme.bg_button, + model.theme.fg_muted, + Msg::Export, + ); + let import_btn = button( + "importar", + model.theme.bg_button, + model.theme.fg_muted, + Msg::Import, + ); + let publish_label = if model.publishing { + "publicando" + } else { + "publicar" + }; + let publish_btn = button( + publish_label, + model.theme.bg_button, + if model.publishing { + model.theme.accent + } else { + model.theme.fg_muted + }, + Msg::Publish, + ); + let receive_btn = button( + "recibir", + model.theme.bg_button, + model.theme.fg_muted, + Msg::Receive, + ); + + View::new(Style { + flex_direction: FlexDirection::Row, + size: Size { + width: percent(1.0_f32), + height: length(HEADER_H), + }, + flex_shrink: 0.0, + padding: Rect { + left: length(0.0_f32), + right: length(10.0_f32), + top: length(4.0_f32), + bottom: length(4.0_f32), + }, + align_items: Some(AlignItems::Center), + gap: Size { + width: length(8.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel_alt) + .children(vec![ + title_node, + list_btn, + new_btn, + archive_btn, + del_btn, + export_btn, + import_btn, + publish_btn, + receive_btn, + ]) +} + +pub(crate) fn button(label: &str, bg: Color, fg: Color, msg: Msg) -> View { + // El ancho crece con el largo del texto — los labels más + // explícitos («+ nueva (Ctrl+N)», «ocultar archivo») piden más + // espacio que un «borrar» seco. + let chars = label.chars().count() as f32; + let width = (chars * 7.2 + 22.0).max(86.0); + View::new(Style { + size: Size { + width: length(width), + height: length(26.0_f32), + }, + padding: Rect { + left: length(10.0_f32), + right: length(10.0_f32), + top: length(0.0_f32), + bottom: length(0.0_f32), + }, + align_items: Some(AlignItems::Center), + justify_content: Some(JustifyContent::Center), + ..Default::default() + }) + .fill(bg) + .radius(4.0) + .text_aligned(label.to_string(), 11.0, fg, Alignment::Center) + .on_click(msg) +} + +pub(crate) fn list_panel( + model: &Model, + palette: &ListPalette, + input_palette: &TextInputPalette, +) -> View { + let now = now_secs(); + let query = model.search.text(); + let q = query.trim(); + + // Particionamos en horizonte vs archivo y ordenamos cada parte por + // masa viva decreciente. Si hay query, ambas listas quedan + // pre-filtradas por coincidencia en título/cuerpo/etiquetas. + let mut visible: Vec<(NoteId, f32, &Note)> = Vec::new(); + let mut archive: Vec<(NoteId, f32, &Note)> = Vec::new(); + let mut hidden_by_query = 0usize; + for id in &model.order { + let Some(n) = model.store.get(*id) else { + continue; + }; + if !q.is_empty() && !note_matches(n, q) { + hidden_by_query += 1; + continue; + } + let m = current_mass(&model.gravity, n, now); + if model.gravity.is_visible(m) { + visible.push((*id, m, n)); + } else { + archive.push((*id, m, n)); + } + } + let by_mass_desc = |a: &(NoteId, f32, &Note), b: &(NoteId, f32, &Note)| { + b.1.partial_cmp(&a.1) + .unwrap_or(core::cmp::Ordering::Equal) + .then(a.0.cmp(&b.0)) + }; + visible.sort_by(by_mass_desc); + archive.sort_by(by_mass_desc); + + let mut chain: Vec<(NoteId, f32, &Note)> = visible.clone(); + if model.show_archive { + chain.extend(archive.iter().cloned()); + } + + let rows: Vec> = chain + .into_iter() + .map(|(id, mass, n)| ListRow { + label: row_label(n, mass), + selected: Some(id) == model.selected, + on_click: Msg::SelectNote(id), + }) + .collect(); + + let caption = if !q.is_empty() { + format!( + "buscar «{}» · {}/{} coinciden", + q, + visible.len() + if model.show_archive { archive.len() } else { 0 }, + visible.len() + archive.len() + hidden_by_query + ) + } else if archive.is_empty() { + format!("notas · {}", visible.len()) + } else if model.show_archive { + format!( + "notas · {} horizonte + {} archivo", + visible.len(), + archive.len() + ) + } else { + format!( + "notas · {} horizonte (+{} archivo)", + visible.len(), + archive.len() + ) + }; + + let spec = ListSpec { + total: rows.len(), + rows, + caption: Some(caption), + truncated_hint: None, + row_height: ROW_H, + palette: *palette, + }; + + let search_input = text_input_view( + &model.search, + "buscar (título, cuerpo, etiquetas)", + model.focus == Focus::Search, + input_palette, + Msg::Focus(Focus::Search), + ); + let search_row = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(28.0_f32), + }, + flex_shrink: 0.0, + padding: Rect { + left: length(6.0_f32), + right: length(6.0_f32), + top: length(4.0_f32), + bottom: length(4.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel_alt) + .children(vec![search_input]); + + let list_wrap = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + flex_basis: length(0.0_f32), + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .children(vec![list_view(spec)]); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: length(LIST_WIDTH), + height: percent(1.0_f32), + }, + flex_shrink: 0.0, + ..Default::default() + }) + .children(vec![search_row, list_wrap]) +} + +/// Panel izquierdo en modo "recibir": arriba un input de dirección manual +/// (`host:puerto`, habilita WAN) con botones jalar/cancelar; debajo, la +/// lista de pares descubiertos en la LAN (click ⇒ jalar de él). Reemplaza +/// transitoriamente la lista de notas. +pub(crate) fn receive_panel( + model: &Model, + palette: &ListPalette, + input_palette: &TextInputPalette, +) -> View { + // Fila de dirección manual + jalar. + let addr_input = text_input_view( + &model.peer_input, + "host:puerto o /ip4/…/p2p/…", + model.focus == Focus::PeerAddr, + input_palette, + Msg::Focus(Focus::PeerAddr), + ); + let addr_wrap = View::new(Style { + size: Size { + width: Dimension::auto(), + height: length(26.0_f32), + }, + flex_grow: 1.0, + ..Default::default() + }) + .children(vec![addr_input]); + let jalar = button( + "jalar", + model.theme.bg_button, + model.theme.accent, + Msg::FetchManual, + ); + let addr_row = View::new(Style { + flex_direction: FlexDirection::Row, + size: Size { + width: percent(1.0_f32), + height: length(34.0_f32), + }, + flex_shrink: 0.0, + padding: Rect { + left: length(6.0_f32), + right: length(6.0_f32), + top: length(4.0_f32), + bottom: length(4.0_f32), + }, + align_items: Some(AlignItems::Center), + gap: Size { + width: length(6.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel_alt) + .children(vec![addr_wrap, jalar]); + + let cancel = button( + "cancelar", + model.theme.bg_button, + model.theme.fg_muted, + Msg::CancelPeers, + ); + let cancel_row = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(34.0_f32), + }, + flex_shrink: 0.0, + padding: Rect { + left: length(6.0_f32), + right: length(6.0_f32), + top: length(2.0_f32), + bottom: length(4.0_f32), + }, + align_items: Some(AlignItems::Center), + ..Default::default() + }) + .fill(model.theme.bg_panel_alt) + .children(vec![cancel]); + + let rows: Vec> = model + .peers + .iter() + .map(|p| ListRow { + label: p.label.clone(), + selected: false, + on_click: Msg::FetchFrom(p.addr.clone()), + }) + .collect(); + let caption = if model.peers.is_empty() { + "pares en la LAN: ninguno aún".to_string() + } else { + format!("pares en la LAN · {} (click para jalar)", model.peers.len()) + }; + let spec = ListSpec { + total: rows.len(), + rows, + caption: Some(caption), + truncated_hint: None, + row_height: ROW_H, + palette: *palette, + }; + let list_wrap = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + flex_basis: length(0.0_f32), + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .children(vec![list_view(spec)]); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: length(LIST_WIDTH), + height: percent(1.0_f32), + }, + flex_shrink: 0.0, + ..Default::default() + }) + .children(vec![addr_row, cancel_row, list_wrap]) +} + +/// Coincidencia sobre título, cuerpo y etiquetas. Case-insensitive. +pub(crate) fn note_matches(n: &Note, query: &str) -> bool { + if n.matches(query) { + return true; + } + let q = query.to_lowercase(); + n.tags.iter().any(|t| t.to_lowercase().contains(&q)) +} + +pub(crate) fn row_label(n: &Note, mass: f32) -> String { + let title = if n.title.is_empty() { + "(sin título)" + } else { + n.title.as_str() + }; + // Una barra de tres bloques visualiza la masa (0..1.5 mapeada a + // 0..3). Sobre el horizonte se ve llena; cayendo, se vacía. + let bars = (mass.clamp(0.0, 1.5) / 0.5).round() as usize; + let glyph: String = (0..3) + .map(|i| if i < bars { '▮' } else { '▯' }) + .collect(); + format!("{glyph} {title}") +} + +pub(crate) fn editor_panel( + model: &Model, + input_palette: &TextInputPalette, + editor_palette: &EditorPalette, +) -> View { + let none_view = || -> View { + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + align_items: Some(AlignItems::Center), + justify_content: Some(JustifyContent::Center), + ..Default::default() + }) + .fill(model.theme.bg_panel) + .text_aligned( + "selecciona o crea una nota".to_string(), + 12.0, + model.theme.fg_muted, + Alignment::Center, + ) + }; + + if model.selected.is_none() { + return wrap_panel(model, none_view()); + } + + let metrics = EditorMetrics::for_font_size(13.0); + + let title_field = field( + model, + "título", + text_input_view( + &model.title, + "(sin título)", + model.focus == Focus::Title, + input_palette, + Msg::Focus(Focus::Title), + ), + ); + + let body_input = text_editor_view( + &model.body, + editor_palette, + metrics, + EDITOR_VISIBLE_LINES, + |ev| Some(Msg::EditorPointer(ev)), + ); + let body_field = body_field_view(model, body_input); + + let tags_field = field( + model, + "etiquetas (coma separadas)", + text_input_view( + &model.tags, + "p. ej. cocina, jardín", + model.focus == Focus::Tags, + input_palette, + Msg::Focus(Focus::Tags), + ), + ); + + let stats = stats_view(model); + + let column = View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: percent(1.0_f32), + }, + padding: Rect { + left: length(12.0_f32), + right: length(12.0_f32), + top: length(10.0_f32), + bottom: length(10.0_f32), + }, + gap: Size { + width: length(0.0_f32), + height: length(8.0_f32), + }, + ..Default::default() + }) + .fill(model.theme.bg_panel) + .children(vec![title_field, body_field, tags_field, stats]); + + wrap_panel(model, column) +} + +pub(crate) fn wrap_panel(_model: &Model, child: View) -> View { + View::new(Style { + size: Size { + width: Dimension::auto(), + height: percent(1.0_f32), + }, + flex_grow: 1.0, + flex_basis: length(0.0_f32), + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + ..Default::default() + }) + .children(vec![child]) +} + +pub(crate) fn field(model: &Model, label: &str, control: View) -> View { + let label_node = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(14.0_f32), + }, + ..Default::default() + }) + .text_aligned( + label.to_string(), + FIELD_LABEL_SIZE, + model.theme.fg_muted, + Alignment::Start, + ); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_shrink: 0.0, + gap: Size { + width: length(0.0_f32), + height: length(2.0_f32), + }, + ..Default::default() + }) + .children(vec![label_node, control]) +} + +pub(crate) fn body_field_view(model: &Model, editor: View) -> View { + let label_node = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(14.0_f32), + }, + ..Default::default() + }) + .text_aligned( + "cuerpo (wiki-links con [[Título]])".to_string(), + FIELD_LABEL_SIZE, + model.theme.fg_muted, + Alignment::Start, + ); + + let focused = model.focus == Focus::Body; + let border = if focused { + model.theme.border_focus + } else { + model.theme.border + }; + + let editor_wrap = View::new(Style { + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + padding: Rect { + left: length(1.0_f32), + right: length(1.0_f32), + top: length(1.0_f32), + bottom: length(1.0_f32), + }, + ..Default::default() + }) + .fill(border) + .radius(4.0) + .on_click(Msg::Focus(Focus::Body)) + .children(vec![editor]); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_grow: 1.0, + min_size: Size { + width: length(0.0_f32), + height: length(0.0_f32), + }, + gap: Size { + width: length(0.0_f32), + height: length(2.0_f32), + }, + ..Default::default() + }) + .children(vec![label_node, editor_wrap]) +} + +pub(crate) fn stats_view(model: &Model) -> View { + let Some(id) = model.selected else { + return View::new(Style::default()); + }; + let fwd = model.store.forward_links(id); + let back = model.store.backlinks(id); + let fwd_titles: Vec = fwd + .iter() + .filter_map(|i| model.store.get(*i).map(|n| n.title.clone())) + .collect(); + let back_titles: Vec = back + .iter() + .filter_map(|i| model.store.get(*i).map(|n| n.title.clone())) + .collect(); + let nearest: Vec = model + .field + .nearest(id, 3) + .into_iter() + .filter_map(|(nid, score)| { + model + .store + .get(nid) + .map(|n| format!("{} ({:.2})", n.title, score)) + }) + .collect(); + + let mut lines = vec![ + format!("→ enlaza a: {}", join_or_dash(&fwd_titles)), + format!("← backlinks: {}", join_or_dash(&back_titles)), + format!("∼ vecinos: {}", join_or_dash(&nearest)), + ]; + // Procedencia: si la nota llegó por compartir, lleva una etiqueta + // `de:`. La mostramos explícita. + if let Some(n) = model.store.get(id) { + let autores: Vec<&str> = n + .tags + .iter() + .filter_map(|t| t.strip_prefix("de:")) + .collect(); + if !autores.is_empty() { + lines.push(format!("✎ de: {}", autores.join(", "))); + } + } + + let nodes: Vec> = lines + .into_iter() + .map(|s| { + View::new(Style { + size: Size { + width: percent(1.0_f32), + height: length(16.0_f32), + }, + ..Default::default() + }) + .text_aligned(s, 11.0, model.theme.fg_muted, Alignment::Start) + }) + .collect(); + + View::new(Style { + flex_direction: FlexDirection::Column, + size: Size { + width: percent(1.0_f32), + height: Dimension::auto(), + }, + flex_shrink: 0.0, + ..Default::default() + }) + .children(nodes) +} + +pub(crate) fn join_or_dash(items: &[String]) -> String { + if items.is_empty() { + "—".to_string() + } else { + items.join(", ") + } +} diff --git a/00_unanchay/khipu/khipu-brahman/Cargo.toml b/00_unanchay/khipu/khipu-brahman/Cargo.toml new file mode 100644 index 0000000..4f085c3 --- /dev/null +++ b/00_unanchay/khipu/khipu-brahman/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "khipu-brahman" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "khipu-brahman — transporte de sobres khipu sobre libp2p (BrahmanNet): stream cifrado /khipu/sobre/1.0.0 + descubrimiento por DHT. El sobre se verifica con khipu-share::open; el transporte no necesita ser confiable." + +[dependencies] +khipu-share = { path = "../khipu-share" } +card-net = { workspace = true } +libp2p = { workspace = true } +libp2p-stream = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +agora-core = { workspace = true } diff --git a/00_unanchay/khipu/khipu-brahman/src/lib.rs b/00_unanchay/khipu/khipu-brahman/src/lib.rs new file mode 100644 index 0000000..922c931 --- /dev/null +++ b/00_unanchay/khipu/khipu-brahman/src/lib.rs @@ -0,0 +1,275 @@ +//! `khipu-brahman` — transporte de sobres khipu sobre libp2p, vía la capa +//! P2P compartida [`card_net::BrahmanNet`] (TCP + Noise + Yamux + Kademlia). +//! +//! Es el hermano WAN del transporte LAN de `khipu-share::net`: en vez de un +//! `TcpStream` directo, abre un stream libp2p sobre el protocolo +//! [`SOBRE_PROTOCOL`] y manda el mismo sobre serializado. Como el sobre va +//! firmado y direccionado por contenido, **el transporte no necesita ser +//! confiable**: quien recibe verifica con [`khipu_share::open`] antes de +//! ingerir. El cifrado Noise sólo agrega confidencialidad en tránsito. +//! +//! El descubrimiento es por DHT Kademlia: [`KhipuNode::anunciar`] publica +//! bajo una clave fija y [`KhipuNode::descubrir`] lista a quién la provee — +//! rendezvous sin saber la `Multiaddr` de antemano (hace falta al menos un +//! peer bootstrap en la tabla, vía [`KhipuNode::add_peer`]). La travesía de +//! NAT (relay + dcutr + autonat) **sí** está cableada en `BrahmanNet` +//! (`shared/card/card-net`); ver el test `jalar_a_traves_de_un_relay` en +//! `tests/p2p_roundtrip.rs`, que jala un sobre a través de un circuito relay. +//! Detrás de NAT simétrico todavía conviene un relay público alcanzable. +//! +//! Marco de cable: `u32` big-endian con el largo del sobre + el sobre +//! (postcard). Un sobre por stream — espejo del marco de `khipu-share::net`. + +use std::sync::Arc; +use std::time::Duration; + +use card_net::{BrahmanNet, Multiaddr, PeerId, Protocol}; +use futures::StreamExt; +use khipu_share::SignedBundle; +use libp2p::StreamProtocol; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +/// Protocolo de stream libp2p para transferir un sobre khipu. +pub const SOBRE_PROTOCOL: StreamProtocol = StreamProtocol::new("/khipu/sobre/1.0.0"); + +/// Clave DHT bajo la que los cuadernos khipu se anuncian y se descubren. +const KHIPU_DHT_KEY: &[u8] = b"khipu/sobre/1.0.0"; + +/// Tope defensivo de un sobre entrante (64 MiB), igual que el transporte LAN. +const MAX_SOBRE: u32 = 64 * 1024 * 1024; + +/// Falla del transporte libp2p de sobres. +#[derive(Debug, thiserror::Error)] +pub enum BrahmanError { + #[error("nodo libp2p: {0}")] + Nodo(String), + #[error("abrir stream: {0}")] + Stream(String), + #[error("marco inválido: {0}")] + Marco(String), + #[error("io de red: {0}")] + Io(String), + #[error("sobre ilegible: {0}")] + Sobre(String), +} + +/// Un cuaderno khipu en la red P2P. Envoltura fina sobre [`BrahmanNet`] +/// que habla el protocolo de sobres. +pub struct KhipuNode { + net: Arc, +} + +impl KhipuNode { + /// Crea un nodo con su propio [`BrahmanNet`] (keypair efímera). Debe + /// llamarse dentro de un runtime de tokio — el swarm corre en una task. + pub fn standalone() -> Result { + let net = BrahmanNet::new().map_err(|e| BrahmanError::Nodo(e.to_string()))?; + Ok(Self { net: Arc::new(net) }) + } + + /// Comparte un [`BrahmanNet`] ya existente (p. ej. el mismo nodo que + /// usan agora/minga), registrando el protocolo de sobres encima. + pub fn sharing(net: Arc) -> Self { + Self { net } + } + + /// Identidad de red de este nodo. + pub fn peer_id(&self) -> PeerId { + self.net.peer_id + } + + /// Empieza a escuchar en `addr`; devuelve la dirección efectiva. + pub async fn listen(&self, addr: Multiaddr) -> Multiaddr { + self.net.listen(addr).await + } + + /// Escucha en una multiaddr dada como texto y devuelve la **dirección + /// para compartir** ya con `/p2p/` — lista para pegarle a un + /// par. Pensada para callers que no quieren tocar tipos de libp2p. + pub async fn listen_str(&self, addr: &str) -> Result { + let m: Multiaddr = addr + .parse() + .map_err(|e| BrahmanError::Nodo(format!("multiaddr inválida: {e}")))?; + let bound = self.net.listen(m).await; + let s = bound.to_string(); + // Una reserva de circuito ya viene con `…/p2p-circuit/p2p/`: + // no le agregamos otro `/p2p/`. Un tcp pelado no trae peer-id, así + // que sí se lo anexamos para que sea una dirección de marcado. + if s.contains("/p2p/") { + Ok(s) + } else { + Ok(format!("{s}/p2p/{}", self.net.peer_id)) + } + } + + /// Marca a un par para conectarse. + pub fn dial(&self, addr: Multiaddr) { + self.net.dial(addr); + } + + /// Marca a un par dado como texto (multiaddr). + pub fn dial_str(&self, addr: &str) -> Result<(), BrahmanError> { + let m: Multiaddr = addr + .parse() + .map_err(|e| BrahmanError::Nodo(format!("multiaddr inválida: {e}")))?; + self.net.dial(m); + Ok(()) + } + + /// Siembra la tabla DHT con un par conocido (bootstrap para descubrir). + pub fn add_peer(&self, peer: PeerId, addr: Multiaddr) { + self.net.add_dht_peer(peer, addr); + } + + /// Anuncia que este nodo sirve un cuaderno khipu (clave DHT fija). + pub fn anunciar(&self) { + self.net.start_providing(KHIPU_DHT_KEY); + } + + /// Descubre nodos khipu por DHT (sin incluirse a sí mismo). + pub async fn descubrir(&self) -> Vec { + let mut peers = self.net.find_providers(KHIPU_DHT_KEY).await; + let me = self.net.peer_id; + peers.retain(|p| *p != me); + peers + } + + /// Sirve sobres: por cada stream entrante llama a `supply` y manda lo + /// que devuelva (típicamente leer `compartido.khipu`). `None` ⇒ no hay + /// sobre, cierra el stream. Bloqueante: corre en su propia task hasta + /// el shutdown del nodo. + pub fn run_serve(&self, supply: F) -> tokio::task::JoinHandle<()> + where + F: Fn() -> Option> + Send + Sync + 'static, + { + let mut control = self.net.control.clone(); + tokio::spawn(async move { + let mut incoming = match control.accept(SOBRE_PROTOCOL) { + Ok(i) => i, + Err(_) => return, // ya hay un accept para este protocolo + }; + let supply = Arc::new(supply); + while let Some((_peer, stream)) = incoming.next().await { + let supply = Arc::clone(&supply); + tokio::spawn(async move { + if let Some(bytes) = supply() { + let mut compat = stream.compat(); + let _ = write_frame(&mut compat, &bytes).await; + } + }); + } + }) + } + + /// Jala el sobre de un par dado por su **dirección de marcado** como + /// texto (`/ip4/.../tcp/.../p2p/`): extrae el peer-id, dial-ea + /// y reintenta el fetch unos segundos mientras se establece la + /// conexión. La forma que usa la app. + pub async fn fetch_addr_str(&self, addr: &str) -> Result { + let m: Multiaddr = addr + .parse() + .map_err(|e| BrahmanError::Nodo(format!("multiaddr inválida: {e}")))?; + let peer = peer_from_multiaddr(&m) + .ok_or_else(|| BrahmanError::Nodo("la multiaddr no trae /p2p/".into()))?; + self.net.dial(m); + let mut intentos = 0u32; + loop { + match self.fetch(peer).await { + Ok(s) => return Ok(s), + Err(_) if intentos < 60 => { + intentos += 1; + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(e) => return Err(e), + } + } + } + + /// Jala el sobre de un par dado por su **peer-id** como texto (la forma + /// que devuelve [`descubrir`](Self::descubrir)). Reintenta mientras el + /// swarm establece la conexión usando las direcciones que aprendió por + /// la DHT/identify. La app la usa para jalar de un par descubierto. + pub async fn fetch_peer_str(&self, peer: &str) -> Result { + let pid: PeerId = peer + .parse() + .map_err(|e| BrahmanError::Nodo(format!("peer-id inválido: {e}")))?; + let mut intentos = 0u32; + loop { + match self.fetch(pid).await { + Ok(s) => return Ok(s), + Err(_) if intentos < 60 => { + intentos += 1; + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(e) => return Err(e), + } + } + } + + /// Abre un stream a `peer` y jala su sobre. **No lo verifica** — el + /// caller debe pasarlo por [`khipu_share::open`] antes de confiar. + pub async fn fetch(&self, peer: PeerId) -> Result { + let mut control = self.net.control.clone(); + let stream = control + .open_stream(peer, SOBRE_PROTOCOL) + .await + .map_err(|e| BrahmanError::Stream(e.to_string()))?; + let mut compat = stream.compat(); + let bytes = read_frame(&mut compat).await?; + SignedBundle::from_bytes(&bytes).map_err(|e| BrahmanError::Sobre(format!("{e:?}"))) + } +} + +/// Extrae el `PeerId` **destino** de una multiaddr: el ÚLTIMO componente +/// `/p2p/`. En una directa (`…/tcp/P/p2p/`) es el único; en un +/// circuito (`…/p2p//p2p-circuit/p2p/`) es el de después +/// del relay, no el relay. +fn peer_from_multiaddr(addr: &Multiaddr) -> Option { + addr.iter() + .filter_map(|p| match p { + Protocol::P2p(id) => Some(id), + _ => None, + }) + .last() +} + +async fn write_frame(stream: &mut S, payload: &[u8]) -> Result<(), BrahmanError> +where + S: AsyncWrite + Unpin, +{ + if payload.len() as u64 > MAX_SOBRE as u64 { + return Err(BrahmanError::Marco("sobre demasiado grande".into())); + } + stream + .write_all(&(payload.len() as u32).to_be_bytes()) + .await + .map_err(|e| BrahmanError::Io(e.to_string()))?; + stream + .write_all(payload) + .await + .map_err(|e| BrahmanError::Io(e.to_string()))?; + stream.flush().await.map_err(|e| BrahmanError::Io(e.to_string()))?; + Ok(()) +} + +async fn read_frame(stream: &mut S) -> Result, BrahmanError> +where + S: AsyncRead + Unpin, +{ + let mut len_buf = [0u8; 4]; + stream + .read_exact(&mut len_buf) + .await + .map_err(|e| BrahmanError::Io(e.to_string()))?; + let len = u32::from_be_bytes(len_buf); + if len > MAX_SOBRE { + return Err(BrahmanError::Marco(format!("largo {len} excede el tope"))); + } + let mut buf = vec![0u8; len as usize]; + stream + .read_exact(&mut buf) + .await + .map_err(|e| BrahmanError::Io(e.to_string()))?; + Ok(buf) +} diff --git a/00_unanchay/khipu/khipu-brahman/tests/p2p_roundtrip.rs b/00_unanchay/khipu/khipu-brahman/tests/p2p_roundtrip.rs new file mode 100644 index 0000000..fed520f --- /dev/null +++ b/00_unanchay/khipu/khipu-brahman/tests/p2p_roundtrip.rs @@ -0,0 +1,193 @@ +//! Integración: transferir un sobre khipu entre dos nodos libp2p reales en +//! localhost. Espeja el molde de `minga-p2p`/`agora-net-brahman`: un nodo +//! escucha y sirve, el otro dial-ea, abre stream y jala; el sobre que llega +//! se verifica con `khipu_share::open`. + +use std::time::{Duration, Instant}; + +use agora_core::Keypair; +use card_net::Multiaddr; +use khipu_brahman::KhipuNode; +use khipu_share::{open, seal, SharedNote}; + +/// Camino que usa la app: `listen_str` da la dirección para compartir y +/// `fetch_addr_str` la consume (dial + reintento + fetch). +#[tokio::test] +async fn jalar_por_direccion_str_como_la_app() { + let autor = Keypair::from_seed([32u8; 32]); + let sobre = seal( + &autor, + vec![SharedNote { + title: "via str".into(), + body: "listen_str + fetch_addr_str".into(), + tags: vec![], + }], + 1, + ) + .unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + let server = KhipuNode::standalone().unwrap(); + let client = KhipuNode::standalone().unwrap(); + let dial = server.listen_str("/ip4/127.0.0.1/tcp/0").await.unwrap(); + let _serve = server.run_serve(move || Some(bytes.clone())); + + // fetch_addr_str ya reintenta internamente mientras se conecta. + let recibido = client.fetch_addr_str(&dial).await.expect("fetch por str"); + let bundle = open(&recibido).expect("verifica tras el viaje"); + assert_eq!(bundle.notes[0].title, "via str"); +} + +/// Descubrimiento por DHT: A publica y se anuncia bajo la clave khipu en +/// la DHT; B —unido a la malla por un rendezvous— la descubre con +/// `descubrir()` y le jala el cuaderno por peer-id, sin conocer su +/// dirección de antemano. +#[tokio::test] +async fn descubrir_por_dht_y_jalar() { + let autor = Keypair::from_seed([34u8; 32]); + let sobre = seal( + &autor, + vec![SharedNote { + title: "via dht".into(), + body: "descubierto por la DHT".into(), + tags: vec![], + }], + 1, + ) + .unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + // Rendezvous: nodo de la malla al que ambos se conectan. + let rendezvous = KhipuNode::standalone().unwrap(); + let r_addr = rendezvous.listen_str("/ip4/127.0.0.1/tcp/0").await.unwrap(); + + // A publica, se une a la malla y se anuncia en la DHT. + let a = KhipuNode::standalone().unwrap(); + let _a_listen = a.listen_str("/ip4/127.0.0.1/tcp/0").await.unwrap(); + let _serve = a.run_serve(move || Some(bytes.clone())); + a.dial_str(&r_addr).unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + a.anunciar(); + tokio::time::sleep(Duration::from_secs(1)).await; + + // B se une por el rendezvous y descubre a A por DHT. + let b = KhipuNode::standalone().unwrap(); + b.dial_str(&r_addr).unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + + let mut peers = Vec::new(); + for _ in 0..20 { + peers = b.descubrir().await; + if peers.contains(&a.peer_id()) { + break; + } + tokio::time::sleep(Duration::from_millis(300)).await; + } + assert!(peers.contains(&a.peer_id()), "B debe descubrir a A por DHT"); + + // B jala de A por su peer-id (la dirección la aprendió por la DHT). + let recibido = tokio::time::timeout( + Duration::from_secs(20), + b.fetch_peer_str(&a.peer_id().to_string()), + ) + .await + .expect("el fetch por peer-id no debería colgar") + .expect("recibir por peer-id descubierto"); + let bundle = open(&recibido).expect("verifica"); + assert_eq!(bundle.notes[0].title, "via dht"); +} + +/// NAT traversal: A reserva un circuito en un relay público y sirve; +/// B le jala el cuaderno *a través del relay* (Circuit Relay v2), sin +/// dirección directa a A. Verifica la maquinaria relay/dcutr de card-net. +#[tokio::test] +async fn jalar_a_traves_de_un_relay() { + let autor = Keypair::from_seed([33u8; 32]); + let sobre = seal( + &autor, + vec![SharedNote { + title: "relay".into(), + body: "viajó por un circuito relay".into(), + tags: vec![], + }], + 1, + ) + .unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + // Relay público. + let relay = KhipuNode::standalone().unwrap(); + let relay_addr = relay.listen_str("/ip4/127.0.0.1/tcp/0").await.unwrap(); + + // A: sirve y reserva un circuito en el relay (su dirección pasa a ser + // `…/p2p//p2p-circuit/p2p/`). + let a = KhipuNode::standalone().unwrap(); + let _serve = a.run_serve(move || Some(bytes.clone())); + // A se conecta al relay; AutoNAT (con el dial-back de A) le confirma al + // relay su dirección externa, necesaria para la reserva. Esperamos a + // que ese sondeo (boot_delay + round-trip) ocurra antes de reservar. + a.dial_str(&relay_addr).unwrap(); + tokio::time::sleep(Duration::from_secs(6)).await; + let circuit = format!("{relay_addr}/p2p-circuit"); + let a_addr = tokio::time::timeout(Duration::from_secs(15), a.listen_str(&circuit)) + .await + .expect("la reserva del circuito no debería colgar") + .unwrap(); + assert!(a_addr.contains("p2p-circuit"), "A debe anunciarse vía circuito"); + + // B jala el cuaderno de A a través del relay. + let b = KhipuNode::standalone().unwrap(); + let recibido = tokio::time::timeout(Duration::from_secs(25), b.fetch_addr_str(&a_addr)) + .await + .expect("el fetch por relay no debería colgar") + .expect("recibir vía relay"); + let bundle = open(&recibido).expect("verifica tras el viaje por relay"); + assert_eq!(bundle.notes[0].title, "relay"); +} + +#[tokio::test] +async fn jalar_un_sobre_entre_dos_nodos_libp2p() { + // Sellar el cuaderno a servir. + let autor = Keypair::from_seed([31u8; 32]); + let sobre = seal( + &autor, + vec![SharedNote { + title: "P2P".into(), + body: "viajó por libp2p".into(), + tags: vec!["brahman".into()], + }], + 1, + ) + .unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + // Dos nodos en localhost. + let server = KhipuNode::standalone().unwrap(); + let client = KhipuNode::standalone().unwrap(); + let server_pid = server.peer_id(); + + let addr = server.listen("/ip4/127.0.0.1/tcp/0".parse().unwrap()).await; + let _serve = server.run_serve(move || Some(bytes.clone())); + + // El cliente dial-ea por multiaddr + peer-id. + let dial: Multiaddr = format!("{addr}/p2p/{server_pid}").parse().unwrap(); + client.dial(dial); + + // Reintentar el fetch hasta que la conexión esté lista. + let deadline = Instant::now() + Duration::from_secs(8); + let recibido = loop { + match client.fetch(server_pid).await { + Ok(s) => break s, + Err(_) if Instant::now() < deadline => { + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(e) => panic!("fetch falló: {e}"), + } + }; + + // Idéntico bit a bit y verificable. + assert_eq!(recibido, sobre); + let bundle = open(&recibido).expect("firma válida tras el viaje libp2p"); + assert_eq!(bundle.notes[0].title, "P2P"); + assert_eq!(bundle.author, autor.public_key()); +} diff --git a/00_unanchay/khipu/khipu-core/Cargo.toml b/00_unanchay/khipu/khipu-core/Cargo.toml new file mode 100644 index 0000000..5597d79 --- /dev/null +++ b/00_unanchay/khipu/khipu-core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "khipu-core" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "khipu-app — núcleo de toma de notas: el modelo Note, el almacén con etiquetas y búsqueda, y el grafo de wiki-links [[...]] con backlinks." + +[dependencies] +serde = { workspace = true } diff --git a/00_unanchay/khipu/khipu-core/LEEME.md b/00_unanchay/khipu/khipu-core/LEEME.md new file mode 100644 index 0000000..b84829f --- /dev/null +++ b/00_unanchay/khipu/khipu-core/LEEME.md @@ -0,0 +1,21 @@ +# khipu-core + +> Modelo de nota + store para [khipu](../README.md). Sin UI. + +`Note` lleva contenido, timestamp de creación, timestamp de último acceso, y `mass: f32`. El store es CRUD simple sobre archivos JSON en `$XDG_DATA_HOME/khipu/`. Cada vez que se lee una nota se actualiza su `last_access` (señal que [khipu-gravity](../khipu-gravity/README.md) usa para reforzar). + +## API + +```rust +use khipu_core::{Note, Store}; + +let store = Store::open()?; +let id = store.create("nota nueva")?; +let note = store.read(id)?; +store.touch(id)?; // refresca last_access +``` + +## Deps + +- `serde`, `serde_json` +- `directories` para el XDG path diff --git a/00_unanchay/khipu/khipu-core/README.md b/00_unanchay/khipu/khipu-core/README.md new file mode 100644 index 0000000..7355ae1 --- /dev/null +++ b/00_unanchay/khipu/khipu-core/README.md @@ -0,0 +1,21 @@ +# khipu-core + +> Note model + store for [khipu](../README.md). No UI. + +`Note` carries content, creation timestamp, last-access timestamp, and `mass: f32`. The store is simple CRUD over JSON files in `$XDG_DATA_HOME/khipu/`. Every read updates `last_access` (the signal [khipu-gravity](../khipu-gravity/README.md) uses to reinforce). + +## API + +```rust +use khipu_core::{Note, Store}; + +let store = Store::open()?; +let id = store.create("new note")?; +let note = store.read(id)?; +store.touch(id)?; // refresh last_access +``` + +## Deps + +- `serde`, `serde_json` +- `directories` for XDG path diff --git a/00_unanchay/khipu/khipu-core/src/lib.rs b/00_unanchay/khipu/khipu-core/src/lib.rs new file mode 100644 index 0000000..9f1d870 --- /dev/null +++ b/00_unanchay/khipu/khipu-core/src/lib.rs @@ -0,0 +1,23 @@ +//! `khipu_app-core` — el núcleo agnóstico de la toma de notas. +//! +//! Una nota es texto con título, etiquetas y enlaces `[[...]]`. El +//! [`NoteStore`] las guarda y deriva el grafo: forward-links, backlinks, +//! huérfanas y enlaces colgantes. Sin UI, sin storage en disco, sin red +//! — tipos puros y deterministas. +//! +//! - [`note`] — el modelo [`Note`]. +//! - [`links`] — el parser de wiki-links `[[...]]`. +//! - [`store`] — el [`NoteStore`] y el grafo de enlaces. +//! +//! La gravedad semántica (clustering por afinidad de embeddings) vive en +//! `khipu_app-gravity`; las lentes visuales, en los crates de frontend. + +#![forbid(unsafe_code)] + +pub mod links; +pub mod note; +pub mod store; + +pub use links::parse_links; +pub use note::{Note, NoteId}; +pub use store::NoteStore; diff --git a/00_unanchay/khipu/khipu-core/src/links.rs b/00_unanchay/khipu/khipu-core/src/links.rs new file mode 100644 index 0000000..df11590 --- /dev/null +++ b/00_unanchay/khipu/khipu-core/src/links.rs @@ -0,0 +1,93 @@ +//! Parser de wiki-links — los destinos `[[...]]` dentro de una nota. +//! +//! Un solo format: dobles corchetes con el título adentro. El texto se +//! recorta; los enlaces vacíos se descartan; el orden de aparición se +//! conserva y se deduplica (un mismo destino enlazado dos veces cuenta +//! una sola vez como arista). + +/// Extrae los destinos `[[...]]` de `text`, recortados y deduplicados, +/// en orden de aparición. +pub fn parse_links(text: &str) -> Vec { + let bytes = text.as_bytes(); + let mut out: Vec = Vec::new(); + let mut i = 0; + while i + 3 < bytes.len() { + if bytes[i] == b'[' && bytes[i + 1] == b'[' { + if let Some(close) = find_close(text, i + 2) { + let inner = text[i + 2..close].trim(); + if !inner.is_empty() && !out.iter().any(|l| l == inner) { + out.push(inner.to_string()); + } + i = close + 2; + continue; + } + } + // Avanza un carácter UTF-8 completo, no un byte. + i += utf8_len(bytes[i]); + } + out +} + +/// Posición del `]]` que cierra a partir de `from`, si existe. +fn find_close(text: &str, from: usize) -> Option { + let bytes = text.as_bytes(); + let mut i = from; + while i + 1 < bytes.len() { + if bytes[i] == b']' && bytes[i + 1] == b']' { + return Some(i); + } + // Un `[[` antes del cierre aborta: enlaces anidados no son válidos. + if bytes[i] == b'[' && bytes[i + 1] == b'[' { + return None; + } + i += 1; + } + None +} + +/// Largo en bytes del carácter UTF-8 que empieza en `b`. +fn utf8_len(b: u8) -> usize { + match b { + 0x00..=0x7F => 1, + 0xC0..=0xDF => 2, + 0xE0..=0xEF => 3, + _ => 4, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extracts_simple_links() { + let links = parse_links("ver [[Cocina]] y también [[Jardín]]."); + assert_eq!(links, vec!["Cocina", "Jardín"]); + } + + #[test] + fn trims_inner_whitespace() { + assert_eq!(parse_links("[[ Taller ]]"), vec!["Taller"]); + } + + #[test] + fn empty_links_are_dropped() { + assert_eq!(parse_links("[[]] y [[ ]]"), Vec::::new()); + } + + #[test] + fn duplicates_collapse_to_one() { + assert_eq!(parse_links("[[A]] [[B]] [[A]]"), vec!["A", "B"]); + } + + #[test] + fn unclosed_bracket_is_ignored() { + assert_eq!(parse_links("texto [[sin cerrar"), Vec::::new()); + } + + #[test] + fn handles_unicode_content_around_links() { + let links = parse_links("café ☕ con [[Niños]] — añoño"); + assert_eq!(links, vec!["Niños"]); + } +} diff --git a/00_unanchay/khipu/khipu-core/src/note.rs b/00_unanchay/khipu/khipu-core/src/note.rs new file mode 100644 index 0000000..f44274c --- /dev/null +++ b/00_unanchay/khipu/khipu-core/src/note.rs @@ -0,0 +1,108 @@ +//! El modelo `Note` — la unidad de khipu_app. + +use serde::{Deserialize, Serialize}; + +use crate::links::parse_links; + +/// Identificador de una nota. Lo asigna el almacén, monótono y estable. +pub type NoteId = u64; + +/// Una nota: título, cuerpo, etiquetas y marcas de tiempo. Los enlaces +/// no se guardan aparte — se derivan del cuerpo bajo demanda. +/// +/// `mass` y `last_access` son la señal temporal: la masa decae con el +/// tiempo y `last_access` registra cuándo fue la última lectura. La +/// física la aplica el caller (típicamente con `khipu-gravity`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Note { + pub id: NoteId, + pub title: String, + pub body: String, + pub tags: Vec, + /// Segundo Unix de creación. + pub created_at: u64, + /// Segundo Unix de la última edición. + pub updated_at: u64, + /// Segundo Unix del último acceso (lectura). Defecto: `created_at` + /// en notas nuevas; en payloads viejos sin el campo, `0` y el + /// caller decide cómo arrancar la masa. + #[serde(default)] + pub last_access: u64, + /// Masa de la nota. 1.0 al crearse; decae con el tiempo, sube con + /// cada acceso. Cuando cae bajo el umbral del horizonte la nota + /// pasa al archivo (no se borra). En payloads viejos arranca en 1.0. + #[serde(default = "default_mass")] + pub mass: f32, + /// Posición ancla en el lienzo del mapa, en coordenadas de mundo. + /// Se fija **una sola vez** cuando la nota se coloca (por gravedad + /// semántica, contra las notas ya asentadas) y se persiste: el mapa + /// es estable — un pensamiento conserva su domicilio para que la + /// memoria espacial pueda recordarlo. `None` = todavía sin colocar + /// (el caller le asigna lugar la primera vez que la pinta). En + /// payloads viejos arranca en `None` y se coloca al cargar. + #[serde(default)] + pub pos: Option<(f32, f32)>, +} + +fn default_mass() -> f32 { + 1.0 +} + +impl Note { + /// Destinos `[[...]]` que el cuerpo de la nota referencia. + pub fn outgoing_links(&self) -> Vec { + parse_links(&self.body) + } + + /// `true` si la nota lleva la etiqueta `tag` (sin distinguir mayúsculas). + pub fn has_tag(&self, tag: &str) -> bool { + self.tags.iter().any(|t| t.eq_ignore_ascii_case(tag)) + } + + /// `true` si `query` aparece en el título o el cuerpo (sin distinguir + /// mayúsculas). + pub fn matches(&self, query: &str) -> bool { + let q = query.to_lowercase(); + self.title.to_lowercase().contains(&q) || self.body.to_lowercase().contains(&q) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn note(title: &str, body: &str) -> Note { + Note { + id: 1, + title: title.into(), + body: body.into(), + tags: vec!["casa".into()], + created_at: 0, + updated_at: 0, + last_access: 0, + mass: 1.0, + pos: None, + } + } + + #[test] + fn outgoing_links_reads_the_body() { + let n = note("Cocina", "preparar con [[Horno]] y [[Cuchillos]]"); + assert_eq!(n.outgoing_links(), vec!["Horno", "Cuchillos"]); + } + + #[test] + fn has_tag_is_case_insensitive() { + let n = note("x", "y"); + assert!(n.has_tag("CASA")); + assert!(!n.has_tag("trabajo")); + } + + #[test] + fn matches_searches_title_and_body() { + let n = note("Lista de mercado", "comprar pan"); + assert!(n.matches("MERCADO")); + assert!(n.matches("pan")); + assert!(!n.matches("ausente")); + } +} diff --git a/00_unanchay/khipu/khipu-core/src/store.rs b/00_unanchay/khipu/khipu-core/src/store.rs new file mode 100644 index 0000000..fc6aa20 --- /dev/null +++ b/00_unanchay/khipu/khipu-core/src/store.rs @@ -0,0 +1,348 @@ +//! `NoteStore` — el almacén de notas y su grafo de enlaces. +//! +//! Guarda las notas en un `BTreeMap` para que toda iteración sea +//! determinista (ordenada por id). Los enlaces se resuelven por título +//! sin distinguir mayúsculas: `[[cocina]]` y `[[Cocina]]` apuntan a la +//! misma nota. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::note::{Note, NoteId}; + +/// El almacén de notas de khipu_app. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NoteStore { + notes: BTreeMap, + /// Siguiente id a asignar — monótono, nunca reutiliza huecos. + next_id: NoteId, +} + +impl NoteStore { + pub fn new() -> Self { + Self { notes: BTreeMap::new(), next_id: 1 } + } + + /// Crea una nota y devuelve su id. Empieza con `mass = 1.0` y + /// `last_access = now` — recién nacida, plenamente visible. + pub fn create( + &mut self, + title: impl Into, + body: impl Into, + tags: Vec, + now: u64, + ) -> NoteId { + let id = self.next_id; + self.next_id += 1; + self.notes.insert( + id, + Note { + id, + title: title.into(), + body: body.into(), + tags, + created_at: now, + updated_at: now, + last_access: now, + mass: 1.0, + pos: None, + }, + ); + id + } + + pub fn len(&self) -> usize { + self.notes.len() + } + + pub fn is_empty(&self) -> bool { + self.notes.is_empty() + } + + pub fn get(&self, id: NoteId) -> Option<&Note> { + self.notes.get(&id) + } + + pub fn get_mut(&mut self, id: NoteId) -> Option<&mut Note> { + self.notes.get_mut(&id) + } + + /// Itera las notas en orden de id (determinista). + pub fn iter(&self) -> impl Iterator { + self.notes.values() + } + + /// Reemplaza el cuerpo de una nota y actualiza su marca de tiempo. + /// También marca `last_access` — editar cuenta como acceso. + /// `false` si la nota no existe. + pub fn update_body(&mut self, id: NoteId, body: impl Into, now: u64) -> bool { + match self.notes.get_mut(&id) { + Some(n) => { + n.body = body.into(); + n.updated_at = now; + n.last_access = now; + true + } + None => false, + } + } + + /// Marca `last_access = now`. La señal que `khipu-gravity` usa para + /// reforzar la masa. No-op si la nota no existe; `true` si tocó. + pub fn touch(&mut self, id: NoteId, now: u64) -> bool { + match self.notes.get_mut(&id) { + Some(n) => { + n.last_access = now; + true + } + None => false, + } + } + + /// Asigna directamente la masa de una nota. La física vive en + /// `khipu-gravity`; el store sólo persiste el resultado. + pub fn set_mass(&mut self, id: NoteId, mass: f32) -> bool { + match self.notes.get_mut(&id) { + Some(n) => { + n.mass = mass; + true + } + None => false, + } + } + + /// Fija la posición ancla de una nota en el lienzo. Se llama una sola + /// vez al colocarla; de ahí en más el domicilio del pensamiento no se + /// mueve salvo que el usuario lo arrastre. `false` si la nota no existe. + pub fn set_pos(&mut self, id: NoteId, x: f32, y: f32) -> bool { + match self.notes.get_mut(&id) { + Some(n) => { + n.pos = Some((x, y)); + true + } + None => false, + } + } + + /// Elimina una nota y la devuelve. + pub fn remove(&mut self, id: NoteId) -> Option { + self.notes.remove(&id) + } + + /// Notas que llevan la etiqueta `tag`, en orden de id. + pub fn by_tag(&self, tag: &str) -> Vec<&Note> { + self.notes.values().filter(|n| n.has_tag(tag)).collect() + } + + /// Notas cuyo título o cuerpo contienen `query`, en orden de id. + pub fn search(&self, query: &str) -> Vec<&Note> { + self.notes.values().filter(|n| n.matches(query)).collect() + } + + /// Ids de las notas cuyo título es `title` (sin distinguir + /// mayúsculas). Pueden ser varias: los títulos no son únicos. + pub fn resolve_title(&self, title: &str) -> Vec { + self.notes + .values() + .filter(|n| n.title.eq_ignore_ascii_case(title)) + .map(|n| n.id) + .collect() + } + + /// Ids de las notas a las que `id` enlaza por `[[...]]`, resueltas y + /// deduplicadas. Un enlace a un título inexistente simplemente no + /// aporta ningún id (enlace colgante). + pub fn forward_links(&self, id: NoteId) -> Vec { + let Some(note) = self.notes.get(&id) else { + return Vec::new(); + }; + let mut out: Vec = Vec::new(); + for title in note.outgoing_links() { + for target in self.resolve_title(&title) { + if !out.contains(&target) { + out.push(target); + } + } + } + out.sort_unstable(); + out + } + + /// Ids de las notas que enlazan hacia `id` (backlinks), en orden de id. + pub fn backlinks(&self, id: NoteId) -> Vec { + let Some(target) = self.notes.get(&id) else { + return Vec::new(); + }; + let title = &target.title; + self.notes + .values() + .filter(|n| { + n.id != id + && n.outgoing_links() + .iter() + .any(|l| l.eq_ignore_ascii_case(title)) + }) + .map(|n| n.id) + .collect() + } + + /// Notas sin ningún backlink — las islas del grafo. + pub fn orphans(&self) -> Vec<&Note> { + self.notes + .values() + .filter(|n| self.backlinks(n.id).is_empty()) + .collect() + } + + /// Destinos `[[...]]` que ninguna nota satisface — enlaces colgantes. + pub fn dangling_links(&self) -> Vec { + let mut out: Vec = Vec::new(); + for note in self.notes.values() { + for title in note.outgoing_links() { + if self.resolve_title(&title).is_empty() + && !out.iter().any(|t| t.eq_ignore_ascii_case(&title)) + { + out.push(title); + } + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Almacén con tres notas enlazadas: Índice → Cocina, Índice → Jardín. + fn seeded() -> (NoteStore, NoteId, NoteId, NoteId) { + let mut s = NoteStore::new(); + let indice = s.create("Índice", "ver [[Cocina]] y [[Jardín]]", vec!["meta".into()], 100); + let cocina = s.create("Cocina", "recetas; vuelve al [[Índice]]", vec!["casa".into()], 100); + let jardin = s.create("Jardín", "plantas y riego", vec!["casa".into()], 100); + (s, indice, cocina, jardin) + } + + #[test] + fn create_assigns_monotonic_ids() { + let (s, indice, cocina, jardin) = seeded(); + assert_eq!((indice, cocina, jardin), (1, 2, 3)); + assert_eq!(s.len(), 3); + } + + #[test] + fn forward_links_resolve_by_title() { + let (s, indice, cocina, jardin) = seeded(); + assert_eq!(s.forward_links(indice), vec![cocina, jardin]); + } + + #[test] + fn backlinks_find_incoming_references() { + let (s, indice, cocina, _) = seeded(); + // Cocina enlaza al Índice → el Índice tiene a Cocina como backlink. + assert_eq!(s.backlinks(indice), vec![cocina]); + } + + #[test] + fn link_resolution_is_case_insensitive() { + let mut s = NoteStore::new(); + let a = s.create("Taller", "trabajo", vec![], 0); + let b = s.create("Notas", "voy al [[taller]]", vec![], 0); + assert_eq!(s.forward_links(b), vec![a]); + assert_eq!(s.backlinks(a), vec![b]); + } + + #[test] + fn by_tag_filters() { + let (s, _, cocina, jardin) = seeded(); + let casa: Vec<_> = s.by_tag("casa").iter().map(|n| n.id).collect(); + assert_eq!(casa, vec![cocina, jardin]); + } + + #[test] + fn search_scans_title_and_body() { + let (s, _, cocina, _) = seeded(); + let hits: Vec<_> = s.search("recetas").iter().map(|n| n.id).collect(); + assert_eq!(hits, vec![cocina]); + } + + #[test] + fn update_body_changes_links_and_timestamp() { + let (mut s, indice, _, jardin) = seeded(); + assert!(s.update_body(indice, "ahora sólo [[Jardín]]", 200)); + assert_eq!(s.forward_links(indice), vec![jardin]); + assert_eq!(s.get(indice).unwrap().updated_at, 200); + } + + #[test] + fn orphans_have_no_backlinks() { + let (s, _, _, jardin) = seeded(); + // Jardín no recibe enlaces de vuelta... pero el Índice sí lo enlaza. + // El único huérfano real sería una nota aislada. + let mut s2 = s; + let aislada = s2.create("Aislada", "sin conexiones", vec![], 0); + let orphan_ids: Vec<_> = s2.orphans().iter().map(|n| n.id).collect(); + assert!(orphan_ids.contains(&aislada)); + assert!(!orphan_ids.contains(&jardin)); + } + + #[test] + fn dangling_links_report_missing_targets() { + let mut s = NoteStore::new(); + s.create("Nota", "apunta a [[Inexistente]]", vec![], 0); + assert_eq!(s.dangling_links(), vec!["Inexistente"]); + } + + #[test] + fn create_initializes_mass_and_last_access() { + let mut s = NoteStore::new(); + let id = s.create("x", "y", vec![], 1_700_000_000); + let n = s.get(id).unwrap(); + assert_eq!(n.mass, 1.0); + assert_eq!(n.last_access, 1_700_000_000); + } + + #[test] + fn touch_refreshes_last_access() { + let mut s = NoteStore::new(); + let id = s.create("x", "y", vec![], 100); + assert!(s.touch(id, 500)); + assert_eq!(s.get(id).unwrap().last_access, 500); + assert!(!s.touch(9_999, 500)); + } + + #[test] + fn update_body_also_marks_last_access() { + let mut s = NoteStore::new(); + let id = s.create("x", "y", vec![], 100); + assert!(s.update_body(id, "z", 700)); + assert_eq!(s.get(id).unwrap().last_access, 700); + } + + #[test] + fn set_mass_persists_the_value() { + let mut s = NoteStore::new(); + let id = s.create("x", "y", vec![], 100); + assert!(s.set_mass(id, 0.42)); + assert!((s.get(id).unwrap().mass - 0.42).abs() < 1e-6); + } + + #[test] + fn set_pos_anchors_the_note_once() { + let mut s = NoteStore::new(); + let id = s.create("x", "y", vec![], 100); + assert_eq!(s.get(id).unwrap().pos, None); + assert!(s.set_pos(id, 12.0, -8.0)); + assert_eq!(s.get(id).unwrap().pos, Some((12.0, -8.0))); + assert!(!s.set_pos(9_999, 0.0, 0.0)); + } + + #[test] + fn remove_drops_the_note() { + let (mut s, indice, ..) = seeded(); + assert!(s.remove(indice).is_some()); + assert_eq!(s.len(), 2); + assert!(s.get(indice).is_none()); + } +} diff --git a/00_unanchay/khipu/khipu-gravity/Cargo.toml b/00_unanchay/khipu/khipu-gravity/Cargo.toml new file mode 100644 index 0000000..2794715 --- /dev/null +++ b/00_unanchay/khipu/khipu-gravity/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "khipu-gravity" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "khipu — dos físicas puras: masa temporal (decay/refuerzo con vida media y horizonte) y gravedad semántica (afinidad coseno, vecinos, clústeres y layout 2D)." + +[dependencies] +khipu-core = { path = "../khipu-core" } +serde = { workspace = true } +# `libm`: la masa decae con `exp` y queremos mantenernos compatibles +# con futuros consumidores `no_std` (kernel wawa, WASM bare). +libm = { workspace = true } diff --git a/00_unanchay/khipu/khipu-gravity/LEEME.md b/00_unanchay/khipu/khipu-gravity/LEEME.md new file mode 100644 index 0000000..410c5f1 --- /dev/null +++ b/00_unanchay/khipu/khipu-gravity/LEEME.md @@ -0,0 +1,20 @@ +# khipu-gravity + +> Algoritmo de masa/decay para [khipu](../README.md). + +Pura física de notas. Cada nota tiene `mass: f32`. Cada tick (configurable, default 1h) la masa decae: `mass *= exp(-dt / half_life)`. Cada acceso la refuerza: `mass += boost`. Cuando `mass < umbral`, la nota cae del horizonte visible (no se borra, queda en archivo). El cálculo es puro — no toca el store; el caller decide qué hacer con el resultado. + +## API + +```rust +use khipu_gravity::{Gravity, Params}; + +let g = Gravity::new(Params::default()); +let new_mass = g.decay(mass, dt); +let new_mass = g.reinforce(mass, boost); +``` + +## Deps + +- `libm` para `exp` (sin `std::math` cuando se compila a WASM) +- Cero deps de I/O o tiempo (el caller pasa `dt`) diff --git a/00_unanchay/khipu/khipu-gravity/README.md b/00_unanchay/khipu/khipu-gravity/README.md new file mode 100644 index 0000000..275e4c1 --- /dev/null +++ b/00_unanchay/khipu/khipu-gravity/README.md @@ -0,0 +1,20 @@ +# khipu-gravity + +> Mass/decay algorithm for [khipu](../README.md). + +Pure physics of notes. Each note has `mass: f32`. Every tick (configurable, default 1h) mass decays: `mass *= exp(-dt / half_life)`. Each access reinforces it: `mass += boost`. When `mass < threshold`, the note falls off the visible horizon (not deleted, kept in archive). Pure computation — doesn't touch the store; the caller decides what to do with the result. + +## API + +```rust +use khipu_gravity::{Gravity, Params}; + +let g = Gravity::new(Params::default()); +let new_mass = g.decay(mass, dt); +let new_mass = g.reinforce(mass, boost); +``` + +## Deps + +- `libm` for `exp` (no `std::math` when compiled to WASM) +- Zero I/O or time deps (caller passes `dt`) diff --git a/00_unanchay/khipu/khipu-gravity/src/lib.rs b/00_unanchay/khipu/khipu-gravity/src/lib.rs new file mode 100644 index 0000000..d97fc3d --- /dev/null +++ b/00_unanchay/khipu/khipu-gravity/src/lib.rs @@ -0,0 +1,479 @@ +//! `khipu-gravity` — dos físicas, ambas puras y deterministas. +//! +//! 1. **Masa temporal** ([`Gravity`] + [`Params`]): cada nota tiene una +//! `mass: f32`. Con el tiempo `decay(mass, dt)` la apaga; con cada +//! acceso `reinforce(mass, boost)` la enciende. Bajo un umbral la +//! nota cae al «archivo» — no se borra, sólo deja el horizonte +//! visible. El cálculo no toca el store; el caller decide qué hacer +//! con el resultado. +//! 2. **Gravedad semántica** ([`SemanticField`]): afinidad coseno entre +//! vectores, vecinos, clústeres por umbral y layout 2D dirigido por +//! fuerzas. Las notas afines se atraen, todas se repelen. +//! +//! Las dos están desacopladas — un caller puede usar sólo una. El +//! crate compila sin alcanzar `std::f32::exp` (usa [`libm`]) para +//! mantenerse apto a futuros consumos `no_std` (kernel wawa, WASM). + +#![forbid(unsafe_code)] + +use khipu_core::NoteId; +use serde::{Deserialize, Serialize}; + +/// Parámetros de la física temporal de una nota. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Params { + /// Vida media: el `dt` (en segundos) tras el cual una masa se + /// reduce a la mitad sin acceso. Default 7 días. + pub half_life_secs: f32, + /// Incremento de masa por acceso. Default 0.4. + pub boost: f32, + /// Bajo este valor la nota cae al archivo (no se borra). + /// Default 0.10. + pub horizon: f32, +} + +impl Default for Params { + fn default() -> Self { + Self { + half_life_secs: 7.0 * 24.0 * 3600.0, + boost: 0.4, + horizon: 0.10, + } + } +} + +/// Física temporal: decay con el tiempo, refuerzo con cada acceso. +/// Pura — no toca reloj ni store. El caller pasa `dt` y aplica el +/// resultado donde corresponda. +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] +pub struct Gravity { + pub params: Params, +} + +impl Gravity { + pub fn new(params: Params) -> Self { + Self { params } + } + + /// Masa decaída tras `dt` segundos sin acceso. `dt` negativo o + /// `half_life_secs <= 0` devuelven la masa intacta. + pub fn decay(&self, mass: f32, dt_secs: f32) -> f32 { + if dt_secs <= 0.0 || self.params.half_life_secs <= 0.0 { + return mass.max(0.0); + } + let k = core::f32::consts::LN_2 / self.params.half_life_secs; + let factor = libm::expf(-k * dt_secs); + (mass * factor).max(0.0) + } + + /// Masa tras un acceso — suma el `boost` configurado. + pub fn reinforce(&self, mass: f32) -> f32 { + (mass + self.params.boost).max(0.0) + } + + /// `true` si la masa está sobre el horizonte (visible). `false` + /// significa archivo, no borrada. + pub fn is_visible(&self, mass: f32) -> bool { + mass >= self.params.horizon + } +} + +/// Una nube de notas con su vector semántico — el dominio de la gravedad. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SemanticField { + /// `(id, vector)` en orden de inserción. + entries: Vec<(NoteId, Vec)>, +} + +/// Posición 2D resultante de una nota tras el layout por gravedad. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct NotePlacement { + pub id: NoteId, + pub x: f32, + pub y: f32, +} + +/// Parámetros del layout dirigido por fuerzas. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct GravityConfig { + /// Pasos de relajación. + pub iterations: usize, + /// Fuerza de atracción entre notas afines. + pub attraction: f32, + /// Fuerza de repulsión entre todo par de notas. + pub repulsion: f32, + /// Radio del círculo de posiciones iniciales. + pub radius: f32, + /// Fracción de la fuerza neta que se aplica por paso (amortiguación). + pub step: f32, +} + +impl Default for GravityConfig { + fn default() -> Self { + Self { + iterations: 120, + attraction: 0.02, + repulsion: 800.0, + radius: 240.0, + step: 0.85, + } + } +} + +/// Similitud coseno de dos vectores. `None` si difieren de largo. +fn cosine(a: &[f32], b: &[f32]) -> Option { + if a.len() != b.len() { + return None; + } + let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum(); + let na: f32 = a.iter().map(|x| x * x).sum::().sqrt(); + let nb: f32 = b.iter().map(|x| x * x).sum::().sqrt(); + if na == 0.0 || nb == 0.0 { + return Some(0.0); + } + Some((dot / (na * nb)).clamp(-1.0, 1.0)) +} + +impl SemanticField { + pub fn new() -> Self { + Self::default() + } + + /// Cantidad de notas en el campo. + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Inserta o reemplaza el vector de una nota. + pub fn insert(&mut self, id: NoteId, vector: Vec) { + if let Some(slot) = self.entries.iter_mut().find(|(eid, _)| *eid == id) { + slot.1 = vector; + } else { + self.entries.push((id, vector)); + } + } + + /// Saca la nota del campo. No-op si no estaba. `true` si se removió. + pub fn remove(&mut self, id: NoteId) -> bool { + let before = self.entries.len(); + self.entries.retain(|(eid, _)| *eid != id); + self.entries.len() != before + } + + /// Itera `(id, &vector)` en orden de inserción. Útil para serializar + /// el campo completo (persistencia, exportar). + pub fn iter(&self) -> impl Iterator { + self.entries.iter().map(|(id, v)| (*id, v.as_slice())) + } + + fn vector_of(&self, id: NoteId) -> Option<&[f32]> { + self.entries + .iter() + .find(|(eid, _)| *eid == id) + .map(|(_, v)| v.as_slice()) + } + + /// Afinidad (similitud coseno) entre dos notas. `None` si alguna no + /// existe o los vectores difieren de largo. + pub fn affinity(&self, a: NoteId, b: NoteId) -> Option { + cosine(self.vector_of(a)?, self.vector_of(b)?) + } + + /// Las `k` notas más afines a `id`, de mayor a menor afinidad. + /// Empata por id ascendente para que el orden sea determinista. + pub fn nearest(&self, id: NoteId, k: usize) -> Vec<(NoteId, f32)> { + let Some(base) = self.vector_of(id) else { + return Vec::new(); + }; + let mut scored: Vec<(NoteId, f32)> = self + .entries + .iter() + .filter(|(eid, _)| *eid != id) + .filter_map(|(eid, v)| cosine(base, v).map(|s| (*eid, s))) + .collect(); + scored.sort_by(|a, b| { + b.1.partial_cmp(&a.1) + .unwrap_or(core::cmp::Ordering::Equal) + .then(a.0.cmp(&b.0)) + }); + scored.truncate(k); + scored + } + + /// Agrupa las notas en clústeres: dos notas quedan en el mismo grupo + /// si su afinidad alcanza `threshold` (transitivamente). Cada + /// clúster viene ordenado por id, y la lista de clústeres también. + pub fn clusters(&self, threshold: f32) -> Vec> { + let n = self.entries.len(); + let mut parent: Vec = (0..n).collect(); + + fn find(parent: &mut [usize], i: usize) -> usize { + let mut root = i; + while parent[root] != root { + root = parent[root]; + } + let mut cur = i; + while parent[cur] != root { + let next = parent[cur]; + parent[cur] = root; + cur = next; + } + root + } + + for i in 0..n { + for j in (i + 1)..n { + let sim = cosine(&self.entries[i].1, &self.entries[j].1).unwrap_or(0.0); + if sim >= threshold { + let (ri, rj) = (find(&mut parent, i), find(&mut parent, j)); + if ri != rj { + parent[ri] = rj; + } + } + } + } + + let mut groups: std::collections::BTreeMap> = Default::default(); + for i in 0..n { + let root = find(&mut parent, i); + groups.entry(root).or_default().push(self.entries[i].0); + } + let mut out: Vec> = groups.into_values().collect(); + for c in &mut out { + c.sort_unstable(); + } + out.sort_by(|a, b| a.first().cmp(&b.first())); + out + } + + /// Layout 2D por gravedad: las notas afines se atraen, todas se + /// repelen. Determinista — posiciones iniciales en círculo, sin RNG. + pub fn gravity_layout(&self, cfg: &GravityConfig) -> Vec { + let n = self.entries.len(); + if n == 0 { + return Vec::new(); + } + if n == 1 { + return vec![NotePlacement { id: self.entries[0].0, x: 0.0, y: 0.0 }]; + } + + // Posiciones iniciales repartidas en un círculo. + let mut pos: Vec<(f32, f32)> = (0..n) + .map(|i| { + let a = core::f32::consts::TAU * i as f32 / n as f32; + (cfg.radius * a.cos(), cfg.radius * a.sin()) + }) + .collect(); + + // Afinidades precomputadas (no cambian entre pasos). + let mut aff = vec![0.0f32; n * n]; + for i in 0..n { + for j in (i + 1)..n { + let s = cosine(&self.entries[i].1, &self.entries[j].1) + .unwrap_or(0.0) + .max(0.0); + aff[i * n + j] = s; + aff[j * n + i] = s; + } + } + + const EPS: f32 = 0.001; + for _ in 0..cfg.iterations { + let mut force = vec![(0.0f32, 0.0f32); n]; + for i in 0..n { + for j in (i + 1)..n { + let dx = pos[j].0 - pos[i].0; + let dy = pos[j].1 - pos[i].1; + let dist = (dx * dx + dy * dy).sqrt().max(EPS); + let (ux, uy) = (dx / dist, dy / dist); + // Atracción crece con la distancia y la afinidad; + // repulsión cae con el cuadrado de la distancia. + let attract = cfg.attraction * aff[i * n + j] * dist; + let repel = cfg.repulsion / (dist * dist); + let net = attract - repel; // >0 → acercar + force[i].0 += net * ux; + force[i].1 += net * uy; + force[j].0 -= net * ux; + force[j].1 -= net * uy; + } + } + for i in 0..n { + pos[i].0 += force[i].0 * cfg.step; + pos[i].1 += force[i].1 * cfg.step; + } + } + + self.entries + .iter() + .zip(pos) + .map(|((id, _), (x, y))| NotePlacement { id: *id, x, y }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tres vectores: 1 y 2 casi paralelos, 3 ortogonal. + fn field() -> SemanticField { + let mut f = SemanticField::new(); + f.insert(1, vec![1.0, 0.0, 0.0]); + f.insert(2, vec![0.9, 0.1, 0.0]); + f.insert(3, vec![0.0, 0.0, 1.0]); + f + } + + #[test] + fn affinity_is_high_for_aligned_vectors() { + let f = field(); + let near = f.affinity(1, 2).unwrap(); + let far = f.affinity(1, 3).unwrap(); + assert!(near > 0.95); + assert!(far.abs() < 1e-6); + assert!(near > far); + } + + #[test] + fn affinity_missing_note_is_none() { + assert!(field().affinity(1, 99).is_none()); + } + + #[test] + fn nearest_ranks_by_affinity() { + let f = field(); + let near = f.nearest(1, 2); + assert_eq!(near[0].0, 2); // el más afín a 1 + assert_eq!(near.len(), 2); + assert!(near[0].1 > near[1].1); + } + + #[test] + fn insert_replaces_existing_vector() { + let mut f = SemanticField::new(); + f.insert(1, vec![1.0, 0.0]); + f.insert(1, vec![0.0, 1.0]); + assert_eq!(f.len(), 1); + assert_eq!(f.vector_of(1), Some([0.0, 1.0].as_slice())); + } + + #[test] + fn clusters_group_affine_notes() { + let f = field(); + // Umbral alto: 1 y 2 juntos, 3 solo. + let cs = f.clusters(0.8); + assert_eq!(cs, vec![vec![1, 2], vec![3]]); + } + + #[test] + fn low_threshold_merges_everything() { + let cs = field().clusters(-1.0); + assert_eq!(cs, vec![vec![1, 2, 3]]); + } + + #[test] + fn gravity_layout_places_every_note() { + let placements = field().gravity_layout(&GravityConfig::default()); + assert_eq!(placements.len(), 3); + let ids: Vec<_> = placements.iter().map(|p| p.id).collect(); + assert_eq!(ids, vec![1, 2, 3]); + } + + #[test] + fn gravity_pulls_affine_notes_closer() { + let f = field(); + let p = f.gravity_layout(&GravityConfig::default()); + let dist = |a: NoteId, b: NoteId| { + let pa = p.iter().find(|x| x.id == a).unwrap(); + let pb = p.iter().find(|x| x.id == b).unwrap(); + ((pa.x - pb.x).powi(2) + (pa.y - pb.y).powi(2)).sqrt() + }; + // Las notas afines (1,2) terminan más cerca que las disímiles (1,3). + assert!(dist(1, 2) < dist(1, 3)); + } + + #[test] + fn gravity_layout_is_deterministic() { + let f = field(); + let a = f.gravity_layout(&GravityConfig::default()); + let b = f.gravity_layout(&GravityConfig::default()); + assert_eq!(a, b); + } + + #[test] + fn remove_drops_a_note() { + let mut f = field(); + assert!(f.remove(2)); + assert_eq!(f.len(), 2); + assert!(!f.remove(99)); + } + + #[test] + fn iter_returns_vectors_in_insertion_order() { + let f = field(); + let collected: Vec<_> = f.iter().map(|(id, v)| (id, v.to_vec())).collect(); + assert_eq!(collected.len(), 3); + assert_eq!(collected[0].0, 1); + assert_eq!(collected[2].0, 3); + } + + #[test] + fn empty_and_single_fields_are_handled() { + assert!(SemanticField::new().gravity_layout(&GravityConfig::default()).is_empty()); + let mut one = SemanticField::new(); + one.insert(7, vec![1.0, 1.0]); + let p = one.gravity_layout(&GravityConfig::default()); + assert_eq!(p, vec![NotePlacement { id: 7, x: 0.0, y: 0.0 }]); + } +} + +#[cfg(test)] +mod mass_tests { + use super::*; + + #[test] + fn decay_halves_mass_after_one_half_life() { + let g = Gravity::new(Params { + half_life_secs: 1000.0, + ..Params::default() + }); + let after = g.decay(1.0, 1000.0); + assert!((after - 0.5).abs() < 1e-3); + } + + #[test] + fn decay_with_zero_dt_is_a_no_op() { + let g = Gravity::default(); + assert!((g.decay(0.7, 0.0) - 0.7).abs() < 1e-6); + } + + #[test] + fn decay_negative_dt_returns_input() { + let g = Gravity::default(); + assert!((g.decay(0.7, -10.0) - 0.7).abs() < 1e-6); + } + + #[test] + fn reinforce_adds_boost() { + let g = Gravity::new(Params { boost: 0.25, ..Params::default() }); + assert!((g.reinforce(0.5) - 0.75).abs() < 1e-6); + } + + #[test] + fn is_visible_uses_horizon() { + let g = Gravity::new(Params { horizon: 0.1, ..Params::default() }); + assert!(g.is_visible(0.10)); + assert!(g.is_visible(0.99)); + assert!(!g.is_visible(0.05)); + } + + #[test] + fn long_decay_keeps_mass_non_negative() { + let g = Gravity::default(); + assert!(g.decay(1.0, 1_000_000_000.0) >= 0.0); + } +} diff --git a/00_unanchay/khipu/khipu-share/Cargo.toml b/00_unanchay/khipu/khipu-share/Cargo.toml new file mode 100644 index 0000000..aa896a6 --- /dev/null +++ b/00_unanchay/khipu/khipu-share/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "khipu-share" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "khipu-share — sobres de notas portables, firmados Ed25519 y direccionados por contenido (BLAKE3) sobre agora. La gravedad temporal es local: se comparte el contenido, no la atención." + +[dependencies] +khipu-core = { path = "../khipu-core" } +agora-core = { workspace = true } +agora-keystore = { workspace = true } +rand = { workspace = true } +blake3 = { workspace = true } +postcard = { workspace = true } +serde = { workspace = true } +serde-big-array = "0.5" +thiserror = { workspace = true } diff --git a/00_unanchay/khipu/khipu-share/src/discovery.rs b/00_unanchay/khipu/khipu-share/src/discovery.rs new file mode 100644 index 0000000..0f17d9a --- /dev/null +++ b/00_unanchay/khipu/khipu-share/src/discovery.rs @@ -0,0 +1,180 @@ +//! Descubrimiento de pares khipu en la LAN por baliza UDP. +//! +//! Un cuaderno que publica emite cada pocos segundos una [`Beacon`] —su +//! clave pública, el puerto TCP donde sirve y un nombre— por broadcast de +//! la LAN y por loopback. Quien quiere recibir escucha una ventana corta, +//! junta los pares vistos y le jala el cuaderno al primero (con +//! [`crate::net::fetch`]), sin tener que saber la IP de antemano. +//! +//! `std::net` puro, best-effort: redes que bloquean broadcast no +//! descubren nada, y ahí sigue valiendo apuntar a un par por dirección +//! explícita. El descubrimiento no afloja la seguridad: la baliza sólo +//! dice "dónde", el sobre que llega por TCP se verifica igual con +//! [`crate::open`]. + +use std::net::{Ipv4Addr, SocketAddr, UdpSocket}; +use std::time::{Duration, Instant}; + +use serde::{Deserialize, Serialize}; + +/// Puerto UDP estándar donde se emiten y escuchan las balizas. +pub const PUERTO_BALIZA: u16 = 7701; + +/// Prefijo que marca un datagrama como baliza khipu — descarta tráfico +/// UDP ajeno antes de intentar parsearlo. +const MAGIA: [u8; 4] = *b"KHPU"; + +/// Lo que un cuaderno anuncia de sí mismo. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Beacon { + /// Clave pública Ed25519 del cuaderno (identidad de quien publica). + pub author: [u8; 32], + /// Puerto TCP donde ese cuaderno sirve el sobre (para `fetch`). + pub port: u16, + /// Nombre legible para mostrar en una lista de pares. + pub name: String, +} + +impl Beacon { + /// Serializa la baliza al cable: `MAGIA` seguido del postcard. + pub fn encode(&self) -> Vec { + let mut out = MAGIA.to_vec(); + if let Ok(body) = postcard::to_allocvec(self) { + out.extend_from_slice(&body); + } + out + } + + /// Parsea un datagrama. `None` si no lleva la magia o no decodifica — + /// así un paquete UDP cualquiera no nos hace ruido. + pub fn decode(bytes: &[u8]) -> Option { + let body = bytes.strip_prefix(&MAGIA)?; + postcard::from_bytes(body).ok() + } +} + +/// Un par visto en la red: dónde jalarle el cuaderno y qué anunció. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PeerVisto { + /// Dirección TCP de `fetch`: la IP de origen del datagrama + el + /// `port` de la baliza. El que anuncia no necesita conocer su propia + /// IP — la deduce quien recibe. + pub fetch_addr: SocketAddr, + pub beacon: Beacon, +} + +/// Emite la baliza una vez: a broadcast de la LAN y a loopback (para que +/// dos cuadernos en la misma máquina se vean). Best-effort — los errores +/// de envío de cada destino se ignoran; sólo falla si no se pudo ni armar +/// el socket emisor. +pub fn anunciar(beacon: &Beacon) -> std::io::Result<()> { + let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?; + sock.set_broadcast(true)?; + let bytes = beacon.encode(); + let _ = sock.send_to(&bytes, (Ipv4Addr::BROADCAST, PUERTO_BALIZA)); + let _ = sock.send_to(&bytes, (Ipv4Addr::LOCALHOST, PUERTO_BALIZA)); + Ok(()) +} + +/// Escucha balizas en un socket ya bindeado durante `ventana`, devolviendo +/// los pares vistos (deduplicados por dirección de fetch). Bloqueante. +/// Separar el socket del bind permite testear sobre un puerto efímero. +pub fn escuchar_en(sock: &UdpSocket, ventana: Duration) -> Vec { + let _ = sock.set_read_timeout(Some(Duration::from_millis(150))); + let fin = Instant::now() + ventana; + let mut vistos: Vec = Vec::new(); + let mut buf = [0u8; 2048]; + while Instant::now() < fin { + match sock.recv_from(&mut buf) { + Ok((n, origen)) => { + if let Some(beacon) = Beacon::decode(&buf[..n]) { + let fetch_addr = SocketAddr::new(origen.ip(), beacon.port); + if !vistos.iter().any(|p| p.fetch_addr == fetch_addr) { + vistos.push(PeerVisto { fetch_addr, beacon }); + } + } + } + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + continue + } + Err(_) => break, + } + } + vistos +} + +/// Bindea el socket de escucha en el puerto estándar de balizas. +pub fn bind_balizas() -> std::io::Result { + UdpSocket::bind((Ipv4Addr::UNSPECIFIED, PUERTO_BALIZA)) +} + +/// Conveniencia: bindea el puerto estándar y escucha `ventana`. +pub fn descubrir(ventana: Duration) -> std::io::Result> { + Ok(escuchar_en(&bind_balizas()?, ventana)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn beacon() -> Beacon { + Beacon { + author: [9u8; 32], + port: 7700, + name: "khipu de prueba".into(), + } + } + + #[test] + fn encode_decode_roundtrips() { + let b = beacon(); + assert_eq!(Beacon::decode(&b.encode()), Some(b)); + } + + #[test] + fn decode_rejects_non_beacon_traffic() { + assert_eq!(Beacon::decode(b"hola mundo"), None); + assert_eq!(Beacon::decode(b"KHP"), None); // magia truncada + assert_eq!(Beacon::decode(&[]), None); + } + + #[test] + fn escuchar_recibe_una_baliza_por_loopback() { + // Escucha en un puerto efímero (no el estándar, para no chocar con + // otros tests ni con un khipu corriendo). + let listener = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let destino = listener.local_addr().unwrap(); + + let b = beacon(); + let emisor = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); + emisor.send_to(&b.encode(), destino).unwrap(); + + let vistos = escuchar_en(&listener, Duration::from_millis(500)); + assert_eq!(vistos.len(), 1); + // La dirección de fetch combina la IP de origen con el port anunciado. + assert_eq!(vistos[0].fetch_addr.ip(), Ipv4Addr::LOCALHOST); + assert_eq!(vistos[0].fetch_addr.port(), b.port); + assert_eq!(vistos[0].beacon, b); + } + + #[test] + fn escuchar_deduplica_balizas_repetidas() { + let listener = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let destino = listener.local_addr().unwrap(); + + let b = beacon(); + let emisor = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); + // Tres balizas idénticas: un solo par visto. + for _ in 0..3 { + emisor.send_to(&b.encode(), destino).unwrap(); + } + + let vistos = escuchar_en(&listener, Duration::from_millis(400)); + assert_eq!(vistos.len(), 1); + } +} diff --git a/00_unanchay/khipu/khipu-share/src/identity.rs b/00_unanchay/khipu/khipu-share/src/identity.rs new file mode 100644 index 0000000..11a6bc3 --- /dev/null +++ b/00_unanchay/khipu/khipu-share/src/identity.rs @@ -0,0 +1,125 @@ +//! Identidad del cuaderno, guardada cifrada con [`agora_keystore`]. +//! +//! La clave privada (la semilla Ed25519 de 32 bytes) nunca queda en claro +//! en disco: vive cifrada bajo una passphrase (Argon2id + ChaCha20-Poly1305). +//! [`unlock`] la descifra a pedido. Quien tenga acceso de lectura al disco +//! no obtiene la identidad sin la passphrase — el endurecimiento frente al +//! `identidad.seed` en claro de las primeras versiones. +//! +//! Migración: si encuentra una semilla legacy en claro, la cifra dentro +//! del keystore y borra el archivo en claro, conservando la identidad. + +use std::path::Path; + +use agora_core::Keypair; +use agora_keystore::Keystore; +use rand::RngCore; + +/// Falla al desbloquear o crear la identidad. +#[derive(Debug, thiserror::Error)] +pub enum IdentityError { + #[error("keystore: {0}")] + Keystore(String), + #[error("semilla legacy inválida (no son 32 bytes)")] + SemillaLegacy, +} + +/// Desbloquea —o crea— la identidad del cuaderno guardada cifrada en +/// `keys_dir`, descifrándola con `passphrase`. Orden de resolución: +/// +/// 1. Si el keystore ya tiene una identidad → la descifra. Passphrase +/// incorrecta ⇒ [`IdentityError::Keystore`] (no se distingue de otros +/// fallos del keystore a propósito: no filtramos si existe o no). +/// 2. Si hay una semilla legacy en claro en `legacy_seed` → la **migra**: +/// la cifra en el keystore con `passphrase`, borra el archivo en claro +/// y devuelve esa identidad. +/// 3. Si no hay nada → genera una identidad nueva (semilla de `OsRng`), +/// la guarda cifrada y la devuelve. +pub fn unlock( + keys_dir: &Path, + legacy_seed: Option<&Path>, + passphrase: &str, +) -> Result { + let ks = Keystore::open(keys_dir).map_err(|e| IdentityError::Keystore(e.to_string()))?; + let ids = ks.list().map_err(|e| IdentityError::Keystore(e.to_string()))?; + + // 1. Identidad ya guardada: descifrar. + if let Some(id) = ids.first().copied() { + let seed = ks + .load(id, passphrase) + .map_err(|e| IdentityError::Keystore(e.to_string()))?; + return Ok(Keypair::from_seed(seed)); + } + + // 2. Migrar una semilla legacy en claro, si la hay. + if let Some(path) = legacy_seed { + if let Ok(bytes) = std::fs::read(path) { + let seed = + <[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| IdentityError::SemillaLegacy)?; + let kp = Keypair::from_seed(seed); + ks.save(kp.identity_id(), &seed, passphrase) + .map_err(|e| IdentityError::Keystore(e.to_string()))?; + let _ = std::fs::remove_file(path); // ya está cifrada; fuera el claro + return Ok(kp); + } + } + + // 3. Identidad nueva. + let mut seed = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut seed); + let kp = Keypair::from_seed(seed); + ks.save(kp.identity_id(), &seed, passphrase) + .map_err(|e| IdentityError::Keystore(e.to_string()))?; + Ok(kp) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Directorio temporal único por nombre de test (evita tempfile como + /// dependencia y las colisiones entre tests del mismo proceso). + fn temp_dir(etiqueta: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("khipu-id-{}-{}", std::process::id(), etiqueta)); + let _ = std::fs::remove_dir_all(&d); + d + } + + #[test] + fn crear_y_desbloquear_da_la_misma_identidad() { + let dir = temp_dir("crear"); + let a = unlock(&dir, None, "secreta").unwrap(); + let b = unlock(&dir, None, "secreta").unwrap(); + assert_eq!(a.public_key(), b.public_key()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn passphrase_incorrecta_falla() { + let dir = temp_dir("malpass"); + let _ = unlock(&dir, None, "correcta").unwrap(); + assert!(unlock(&dir, None, "incorrecta").is_err()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn migra_semilla_legacy_en_claro() { + let base = temp_dir("migra"); + std::fs::create_dir_all(&base).unwrap(); + let legacy = base.join("identidad.seed"); + let semilla = [7u8; 32]; + std::fs::write(&legacy, semilla).unwrap(); + + let keys = base.join("keys"); + let kp = unlock(&keys, Some(&legacy), "secreta").unwrap(); + // Conserva la identidad de la semilla legacy. + assert_eq!(kp.public_key(), Keypair::from_seed(semilla).public_key()); + // El archivo en claro fue borrado. + assert!(!legacy.exists()); + // Y ahora se descifra del keystore, sin legacy. + let kp2 = unlock(&keys, None, "secreta").unwrap(); + assert_eq!(kp2.public_key(), kp.public_key()); + + let _ = std::fs::remove_dir_all(&base); + } +} diff --git a/00_unanchay/khipu/khipu-share/src/lib.rs b/00_unanchay/khipu/khipu-share/src/lib.rs new file mode 100644 index 0000000..65ab1c0 --- /dev/null +++ b/00_unanchay/khipu/khipu-share/src/lib.rs @@ -0,0 +1,349 @@ +//! `khipu-share` — compartir notas por la red soberana de gioser sin +//! perder su gravedad local. +//! +//! Lo que viaja es el **contenido** de la nota (título, cuerpo, etiquetas), +//! nunca su física temporal: la masa y el último acceso son la atención +//! *de quien tiene la nota*, no una propiedad transferible. Al importar, +//! cada nota nace fresca (`mass = 1.0`, `last_access = now`) — su gravedad +//! arranca en el cuaderno que la recibe. +//! +//! El sobre es: +//! - **direccionado por contenido**: su identidad es `BLAKE3(postcard(bundle))`, +//! así dos sobres con las mismas notas tienen el mismo hash; +//! - **firmado Ed25519** sobre ese hash con la clave del autor, vía +//! [`agora_core`] — verificable sin autoridad central ni red. +//! +//! ``` +//! use agora_core::Keypair; +//! use khipu_share::{seal, open, SharedNote}; +//! +//! let kp = Keypair::from_seed([7u8; 32]); +//! let notas = vec![SharedNote { +//! title: "Receta".into(), +//! body: "sopa; ver [[Mercado]]".into(), +//! tags: vec!["cocina".into()], +//! }]; +//! let sobre = seal(&kp, notas, 1_700_000_000).unwrap(); +//! +//! // El receptor verifica firma + hash antes de confiar en el contenido. +//! let bundle = open(&sobre).unwrap(); +//! assert_eq!(bundle.notes[0].title, "Receta"); +//! ``` + +#![forbid(unsafe_code)] + +pub mod discovery; +pub mod identity; +pub mod net; + +use std::collections::HashSet; + +use agora_core::{verify_signature, Keypair}; +use khipu_core::{Note, NoteId, NoteStore}; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; + +/// El contenido transferible de una nota — sin id, masa ni timestamps. +/// La gravedad temporal se queda en el cuaderno de origen. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SharedNote { + pub title: String, + pub body: String, + pub tags: Vec, +} + +impl SharedNote { + /// Proyecta una [`Note`] local a su contenido compartible, descartando + /// id, masa y marcas de tiempo. + pub fn from_note(n: &Note) -> Self { + Self { + title: n.title.clone(), + body: n.body.clone(), + tags: n.tags.clone(), + } + } +} + +/// El cuerpo firmable: el autor, cuándo se selló, y las notas. Es lo que +/// se serializa y se hashea — el orden de las notas y las etiquetas es +/// significativo para que el hash sea estable. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bundle { + /// Clave pública Ed25519 del autor (32 bytes). + pub author: [u8; 32], + /// Segundo Unix en que se selló el sobre. + pub created_at: u64, + pub notes: Vec, +} + +/// Un [`Bundle`] con la firma del autor sobre su hash de contenido. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedBundle { + pub bundle: Bundle, + /// Firma Ed25519 sobre [`Bundle::content_hash`]. + #[serde(with = "BigArray")] + pub signature: [u8; 64], +} + +impl Bundle { + /// La dirección de contenido del sobre: `BLAKE3(postcard(self))`. Es + /// también el mensaje que el autor firma. + pub fn content_hash(&self) -> Result<[u8; 32], ShareError> { + let bytes = postcard::to_allocvec(self).map_err(|_| ShareError::Serializacion)?; + Ok(*blake3::hash(&bytes).as_bytes()) + } +} + +impl SignedBundle { + /// Serializa el sobre a bytes (postcard) para escribirlo a disco o + /// mandarlo por cualquier canal. + pub fn to_bytes(&self) -> Result, ShareError> { + postcard::to_allocvec(self).map_err(|_| ShareError::Serializacion) + } + + /// Reconstruye un sobre desde bytes. No verifica la firma — para eso + /// está [`open`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + postcard::from_bytes(bytes).map_err(|_| ShareError::Serializacion) + } + + /// La dirección de contenido del sobre (delegada a [`Bundle::content_hash`]). + pub fn content_address(&self) -> Result<[u8; 32], ShareError> { + self.bundle.content_hash() + } +} + +/// Sella las notas en un sobre firmado por `kp`. +pub fn seal( + kp: &Keypair, + notes: Vec, + created_at: u64, +) -> Result { + let bundle = Bundle { + author: kp.public_key(), + created_at, + notes, + }; + let hash = bundle.content_hash()?; + let signature = kp.sign(&hash); + Ok(SignedBundle { bundle, signature }) +} + +/// Abre un sobre: recomputa su hash de contenido y verifica que la firma +/// corresponda a la clave del autor declarado. Devuelve el [`Bundle`] +/// verificado, o un error si el contenido fue alterado o la firma no +/// corresponde. Verificación offline — sin autoridad central. +pub fn open(signed: &SignedBundle) -> Result<&Bundle, ShareError> { + let hash = signed.bundle.content_hash()?; + verify_signature(&signed.bundle.author, &hash, &signed.signature) + .map_err(|_| ShareError::FirmaInvalida)?; + Ok(&signed.bundle) +} + +/// Resultado de ingerir un sobre en un cuaderno. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ImportOutcome { + /// Ids de las notas efectivamente creadas. + pub created: Vec, + /// Notas omitidas por título ya presente (la importación repetida es + /// inofensiva — no duplica). + pub skipped: usize, +} + +/// Inserta las notas de un `bundle` en `store` como notas nuevas: id e +/// historia local frescos, `mass = 1.0`, `last_access = now`. La gravedad +/// arranca en el cuaderno receptor. +/// +/// Las notas cuyo título (sin distinguir mayúsculas) ya existe en el +/// cuaderno se omiten, de modo que importar dos veces el mismo sobre no +/// duplica. Los wiki-links `[[Título]]` sobreviven solos: khipu resuelve +/// enlaces por título, así que si las notas enlazadas también se importan, +/// el grafo se rearma sin remapear ids. +/// +/// Cada nota importada recibe una etiqueta de procedencia [`tag_de`] +/// (`de:`) — así queda asentado de quién vino sin tocar +/// el modelo `Note` ni la persistencia. Es una etiqueta normal: visible, +/// buscable y removible. +pub fn import_into(store: &mut NoteStore, bundle: &Bundle, now: u64) -> ImportOutcome { + let mut existing: HashSet = + store.iter().map(|n| n.title.to_lowercase()).collect(); + let marca = tag_de(&bundle.author); + let mut out = ImportOutcome::default(); + for sn in &bundle.notes { + let key = sn.title.to_lowercase(); + // Sólo deduplicamos por títulos no vacíos: dos "(sin título)" no + // son la misma nota. + if !sn.title.is_empty() && existing.contains(&key) { + out.skipped += 1; + continue; + } + let mut tags = sn.tags.clone(); + if !tags.iter().any(|t| t == &marca) { + tags.push(marca.clone()); + } + let id = store.create(sn.title.clone(), sn.body.clone(), tags, now); + existing.insert(key); + out.created.push(id); + } + out +} + +/// Prefijo hex (4 bytes / 8 hex) de una clave o hash — identifica un +/// autor de forma legible sin volcar los 32 bytes. +pub fn hex8(bytes: &[u8; 32]) -> String { + bytes[..4].iter().map(|b| format!("{b:02x}")).collect() +} + +/// La etiqueta de procedencia para un autor: `de:`. +pub fn tag_de(author: &[u8; 32]) -> String { + format!("de:{}", hex8(author)) +} + +/// Falla al sellar, abrir o transportar un sobre. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum ShareError { + #[error("no se pudo (de)serializar el sobre")] + Serializacion, + #[error("la firma no corresponde al contenido y la clave del autor")] + FirmaInvalida, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn nota(title: &str, body: &str, tags: &[&str]) -> SharedNote { + SharedNote { + title: title.into(), + body: body.into(), + tags: tags.iter().map(|s| s.to_string()).collect(), + } + } + + #[test] + fn seal_then_open_roundtrips() { + let kp = Keypair::from_seed([1u8; 32]); + let notes = vec![nota("A", "cuerpo a", &["x"]), nota("B", "[[A]]", &[])]; + let sobre = seal(&kp, notes.clone(), 42).unwrap(); + let bundle = open(&sobre).unwrap(); + assert_eq!(bundle.author, kp.public_key()); + assert_eq!(bundle.created_at, 42); + assert_eq!(bundle.notes, notes); + } + + #[test] + fn content_hash_is_stable_and_content_addressed() { + let kp = Keypair::from_seed([2u8; 32]); + let notes = vec![nota("A", "a", &["t"])]; + let a = seal(&kp, notes.clone(), 100).unwrap(); + let b = seal(&kp, notes, 100).unwrap(); + // Mismas notas + mismo autor + mismo instante → mismo hash y firma. + assert_eq!(a.content_address().unwrap(), b.content_address().unwrap()); + assert_eq!(a.signature, b.signature); + } + + #[test] + fn tampering_with_content_is_rejected() { + let kp = Keypair::from_seed([3u8; 32]); + let mut sobre = seal(&kp, vec![nota("real", "intacto", &[])], 1).unwrap(); + // Alteramos el cuerpo sin rehacer la firma. + sobre.bundle.notes[0].body = "manipulado".into(); + assert_eq!(open(&sobre), Err(ShareError::FirmaInvalida)); + } + + #[test] + fn forged_author_is_rejected() { + let autor = Keypair::from_seed([4u8; 32]); + let impostor = Keypair::from_seed([5u8; 32]); + let mut sobre = seal(&autor, vec![nota("n", "c", &[])], 1).unwrap(); + // El impostor pone su clave pero no puede producir la firma válida. + sobre.bundle.author = impostor.public_key(); + assert_eq!(open(&sobre), Err(ShareError::FirmaInvalida)); + } + + #[test] + fn bytes_roundtrip_preserves_verification() { + let kp = Keypair::from_seed([6u8; 32]); + let sobre = seal(&kp, vec![nota("n", "c", &["a", "b"])], 9).unwrap(); + let bytes = sobre.to_bytes().unwrap(); + let recuperado = SignedBundle::from_bytes(&bytes).unwrap(); + assert_eq!(recuperado, sobre); + assert!(open(&recuperado).is_ok()); + } + + #[test] + fn import_creates_fresh_notes_with_local_gravity() { + let kp = Keypair::from_seed([7u8; 32]); + let sobre = seal(&kp, vec![nota("Receta", "sopa", &["cocina"])], 1).unwrap(); + let bundle = open(&sobre).unwrap(); + + let mut store = NoteStore::new(); + let out = import_into(&mut store, bundle, 5_000); + assert_eq!(out.created.len(), 1); + assert_eq!(out.skipped, 0); + + let n = store.get(out.created[0]).unwrap(); + assert_eq!(n.title, "Receta"); + // Conserva sus etiquetas y suma la de procedencia. + assert!(n.tags.contains(&"cocina".to_string())); + assert!(n.tags.iter().any(|t| t.starts_with("de:"))); + // Gravedad fresca: masa plena y acceso = ahora del receptor. + assert_eq!(n.mass, 1.0); + assert_eq!(n.last_access, 5_000); + assert_eq!(n.created_at, 5_000); + } + + #[test] + fn import_marks_author_provenance() { + let kp = Keypair::from_seed([13u8; 32]); + let sobre = seal(&kp, vec![nota("N", "c", &[])], 1).unwrap(); + let bundle = open(&sobre).unwrap(); + let mut store = NoteStore::new(); + let out = import_into(&mut store, bundle, 1); + let n = store.get(out.created[0]).unwrap(); + assert!(n.tags.contains(&tag_de(&kp.public_key()))); + } + + #[test] + fn reimport_skips_existing_titles() { + let kp = Keypair::from_seed([8u8; 32]); + let sobre = seal( + &kp, + vec![nota("A", "a", &[]), nota("B", "b", &[])], + 1, + ) + .unwrap(); + let bundle = open(&sobre).unwrap(); + + let mut store = NoteStore::new(); + let first = import_into(&mut store, bundle, 10); + assert_eq!(first.created.len(), 2); + + // Segunda importación del mismo sobre: nada nuevo. + let second = import_into(&mut store, bundle, 20); + assert_eq!(second.created.len(), 0); + assert_eq!(second.skipped, 2); + assert_eq!(store.len(), 2); + } + + #[test] + fn wiki_links_survive_import_by_title() { + let kp = Keypair::from_seed([9u8; 32]); + let sobre = seal( + &kp, + vec![ + nota("Índice", "ver [[Receta]]", &[]), + nota("Receta", "sopa", &[]), + ], + 1, + ) + .unwrap(); + let bundle = open(&sobre).unwrap(); + + let mut store = NoteStore::new(); + let out = import_into(&mut store, bundle, 1); + let indice = out.created[0]; + // El enlace [[Receta]] resuelve a la otra nota importada. + assert_eq!(store.forward_links(indice).len(), 1); + } +} diff --git a/00_unanchay/khipu/khipu-share/src/net.rs b/00_unanchay/khipu/khipu-share/src/net.rs new file mode 100644 index 0000000..583cc88 --- /dev/null +++ b/00_unanchay/khipu/khipu-share/src/net.rs @@ -0,0 +1,155 @@ +//! Transporte LAN mínimo para sobres khipu sobre TCP. +//! +//! El sobre ya es firmado y direccionado por contenido, así que el +//! transporte no necesita ser confiable: quien recibe verifica con +//! [`crate::open`] antes de creer nada. Esto es `std::net` puro — sin +//! libp2p ni async — pensado para "jalar el cuaderno de un par en la LAN". +//! +//! Marco de cable: un `u32` big-endian con el largo del sobre, seguido +//! del sobre serializado (postcard). Un sobre por conexión. + +use std::io::{self, Read, Write}; +use std::net::{TcpListener, TcpStream, ToSocketAddrs}; + +use crate::{ShareError, SignedBundle}; + +/// Tope defensivo para un sobre entrante (64 MiB): evita que un par +/// hostil pida un alloc gigante declarando un largo inflado. +const MAX_SOBRE: u32 = 64 * 1024 * 1024; + +/// Falla del transporte de sobres. +#[derive(Debug, thiserror::Error)] +pub enum NetError { + #[error("io de red: {0}")] + Io(String), + #[error("marco inválido: {0}")] + Protocolo(String), + #[error(transparent)] + Sobre(#[from] ShareError), +} + +impl From for NetError { + fn from(e: io::Error) -> Self { + NetError::Io(e.to_string()) + } +} + +fn write_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + if payload.len() as u64 > MAX_SOBRE as u64 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "sobre demasiado grande para el marco", + )); + } + stream.write_all(&(payload.len() as u32).to_be_bytes())?; + stream.write_all(payload)?; + stream.flush() +} + +fn read_frame(stream: &mut TcpStream) -> Result, NetError> { + let mut len_buf = [0u8; 4]; + stream.read_exact(&mut len_buf)?; + let len = u32::from_be_bytes(len_buf); + if len > MAX_SOBRE { + return Err(NetError::Protocolo(format!( + "largo declarado {len} excede el tope" + ))); + } + let mut buf = vec![0u8; len as usize]; + stream.read_exact(&mut buf)?; + Ok(buf) +} + +/// Conecta a `addr`, lee un sobre y lo deserializa. **No lo verifica** — +/// el caller debe pasar el resultado por [`crate::open`] antes de confiar. +pub fn fetch(addr: impl ToSocketAddrs) -> Result { + let mut stream = TcpStream::connect(addr)?; + let bytes = read_frame(&mut stream)?; + Ok(SignedBundle::from_bytes(&bytes)?) +} + +/// Atiende una sola conexión: manda `payload` y vuelve. Útil para tests +/// y para un "compartir una vez". +pub fn serve_once(listener: &TcpListener, payload: &[u8]) -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + write_frame(&mut stream, payload) +} + +/// Atiende conexiones para siempre. Por cada una llama a `supply` para +/// obtener los bytes a mandar — típicamente leer `compartido.khipu` del +/// disco, así sirve siempre la versión vigente. Una conexión cuyo +/// `supply` falla (todavía no hay sobre, p. ej.) se salta sin tumbar el +/// servidor. Bloqueante: pensado para correr en su propio hilo. +pub fn serve_loop(listener: TcpListener, supply: F) +where + F: Fn() -> io::Result>, +{ + for conn in listener.incoming() { + let Ok(mut stream) = conn else { continue }; + if let Ok(bytes) = supply() { + let _ = write_frame(&mut stream, &bytes); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{open, seal, SharedNote}; + use agora_core::Keypair; + + fn nota(title: &str, body: &str) -> SharedNote { + SharedNote { + title: title.into(), + body: body.into(), + tags: Vec::new(), + } + } + + #[test] + fn fetch_recovers_a_served_bundle_and_verifies() { + let kp = Keypair::from_seed([11u8; 32]); + let sobre = seal(&kp, vec![nota("Red", "hola por TCP")], 1).unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || serve_once(&listener, &bytes).unwrap()); + + let recibido = fetch(addr).unwrap(); + server.join().unwrap(); + + assert_eq!(recibido, sobre); + // El sobre que llegó por la red verifica firma + hash. + let bundle = open(&recibido).unwrap(); + assert_eq!(bundle.notes[0].title, "Red"); + } + + #[test] + fn serve_loop_serves_the_current_supply_each_time() { + let kp = Keypair::from_seed([12u8; 32]); + let sobre = seal(&kp, vec![nota("A", "a")], 1).unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + // El servidor corre en su hilo; lo dejamos colgado al terminar el + // test (los dos fetch ya probaron lo que importa). + std::thread::spawn(move || { + serve_loop(listener, move || Ok(bytes.clone())); + }); + + // Dos pares distintos jalan el mismo cuaderno. + let a = fetch(addr).unwrap(); + let b = fetch(addr).unwrap(); + assert_eq!(a, sobre); + assert_eq!(b, sobre); + } + + #[test] + fn fetch_against_nothing_is_an_error_not_a_panic() { + // Puerto cerrado: connect falla, devolvemos NetError::Io. + let err = fetch("127.0.0.1:1").unwrap_err(); + assert!(matches!(err, NetError::Io(_))); + } +} diff --git a/00_unanchay/khipu/khipu-share/tests/lan_roundtrip.rs b/00_unanchay/khipu/khipu-share/tests/lan_roundtrip.rs new file mode 100644 index 0000000..b4372a2 --- /dev/null +++ b/00_unanchay/khipu/khipu-share/tests/lan_roundtrip.rs @@ -0,0 +1,75 @@ +//! Integración: el camino completo de compartir por LAN, todo en loopback. +//! +//! Sella un cuaderno → lo sirve por TCP → anuncia su baliza → otro par lo +//! descubre por UDP, le jala el sobre y lo verifica. Es la cadena que +//! ejercitan los botones «publicar» y «recibir» de khipu-app, sin GUI. + +use std::net::{Ipv4Addr, TcpListener, UdpSocket}; +use std::time::Duration; + +use agora_core::Keypair; +use khipu_core::NoteStore; +use khipu_share::discovery::{escuchar_en, Beacon}; +use khipu_share::{net, open, seal, SharedNote}; + +#[test] +fn descubrir_jalar_y_verificar_de_punta_a_punta() { + // --- Lado que publica --- + let autor = Keypair::from_seed([21u8; 32]); + let sobre = seal( + &autor, + vec![SharedNote { + title: "Compartida por LAN".into(), + body: "viajó por descubrimiento + TCP".into(), + tags: vec!["red".into()], + }], + 100, + ) + .unwrap(); + let bytes = sobre.to_bytes().unwrap(); + + // Sirve el sobre por TCP en un puerto efímero. + let tcp = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let tcp_port = tcp.local_addr().unwrap().port(); + std::thread::spawn(move || { + let _ = net::serve_once(&tcp, &bytes); + }); + + // Anuncia la baliza apuntando a ese puerto TCP. + let beacon = Beacon { + author: autor.public_key(), + port: tcp_port, + name: "khipu publicador".into(), + }; + + // --- Lado que recibe --- + // Escucha balizas en un puerto UDP efímero (en la app sería el + // estándar 7701); el publicador le manda la suya por loopback. + let listener = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let destino = listener.local_addr().unwrap(); + let emisor = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); + emisor.send_to(&beacon.encode(), destino).unwrap(); + + // Descubre al par y le jala el cuaderno. + let pares = escuchar_en(&listener, Duration::from_millis(500)); + assert_eq!(pares.len(), 1, "debería haber descubierto un par"); + let par = &pares[0]; + assert_eq!(par.fetch_addr.port(), tcp_port); + assert_eq!(par.beacon.author, autor.public_key()); + + let recibido = net::fetch(par.fetch_addr).unwrap(); + + // Verifica firma + hash e ingiere como nota fresca. + let bundle = open(&recibido).unwrap(); + let mut store = NoteStore::new(); + let resultado = khipu_share::import_into(&mut store, bundle, 9_000); + assert_eq!(resultado.created.len(), 1); + let nota = store.get(resultado.created[0]).unwrap(); + assert_eq!(nota.title, "Compartida por LAN"); + assert!(nota.tags.contains(&"red".to_string())); + // La nota lleva la procedencia del autor que la selló. + assert!(nota.tags.contains(&khipu_share::tag_de(&autor.public_key()))); + // Gravedad fresca en el receptor. + assert_eq!(nota.mass, 1.0); + assert_eq!(nota.last_access, 9_000); +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..85613de --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6692 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "agora-core" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "blake3", + "ed25519-dalek", + "format", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "agora-keystore" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "aead", + "agora-core", + "argon2", + "chacha20poly1305", + "directories", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.12.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "app-bus" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "directories", + "serde", + "toml", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.12.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "card-net" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "blake3", + "futures", + "libp2p", + "libp2p-allow-block-list", + "libp2p-stream", + "serde", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-cache-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f8afb20c8069fd676d27b214559a337cc619a605d25a87baa90b49a06f3b18" +dependencies = [ + "bytemuck", + "thiserror 1.0.69", +] + +[[package]] +name = "fontique" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64763d1f274c8383333851435b6cdf071c31cfcdb39fd5860d20943205a007a7" +dependencies = [ + "bytemuck", + "fontconfig-cache-parser", + "hashbrown 0.15.5", + "icu_locid", + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "peniko", + "read-fonts 0.29.3", + "roxmltree", + "smallvec", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "format" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "blake3", + "postcard", + "serde", + "serde-big-array", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.12.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.12.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "grid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "socket2 0.5.10", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap 0.8.2", + "tinystr 0.8.3", + "writeable 0.6.3", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap 0.7.5", + "tinystr 0.7.6", + "writeable 0.5.5", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable 0.6.3", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "if-watch" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" +dependencies = [ + "async-io", + "core-foundation", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration", + "tokio", + "windows 0.62.2", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.4", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.4", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khipu-app" +version = "0.1.0" +dependencies = [ + "agora-core", + "app-bus", + "directories", + "khipu-brahman", + "khipu-core", + "khipu-gravity", + "khipu-share", + "llimphi-clipboard", + "llimphi-motion", + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-context-menu", + "llimphi-widget-edit-menu", + "llimphi-widget-list", + "llimphi-widget-menubar", + "llimphi-widget-text-editor", + "llimphi-widget-text-input", + "llimphi-widget-tiled", + "postcard", + "rimay-verbo", + "serde", + "tokio", +] + +[[package]] +name = "khipu-brahman" +version = "0.1.0" +dependencies = [ + "agora-core", + "card-net", + "futures", + "khipu-share", + "libp2p", + "libp2p-stream", + "thiserror 2.0.18", + "tokio", + "tokio-util", +] + +[[package]] +name = "khipu-core" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "khipu-gravity" +version = "0.1.0" +dependencies = [ + "khipu-core", + "libm", + "serde", +] + +[[package]] +name = "khipu-share" +version = "0.1.0" +dependencies = [ + "agora-core", + "agora-keystore", + "blake3", + "khipu-core", + "postcard", + "rand 0.8.6", + "serde", + "serde-big-array", + "thiserror 2.0.18", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libp2p" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.17", + "libp2p-allow-block-list", + "libp2p-autonat", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dcutr", + "libp2p-dns", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-noise", + "libp2p-quic", + "libp2p-relay", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "libp2p-yamux", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 2.0.18", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-autonat" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab5e25c49a7d48dac83d95d8f3bac0a290d8a5df717012f6e34ce9886396c0b" +dependencies = [ + "async-trait", + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-request-response", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "rand_core 0.6.4", + "thiserror 2.0.18", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-core" +version = "0.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.6", + "rw-stream-sink", + "thiserror 2.0.18", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + +[[package]] +name = "libp2p-dcutr" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4107305e12158af3e66960b6181789c547394c9c9a8696f721521602bfc73a" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "hashlink", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "thiserror 2.0.18", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver", + "libp2p-core", + "libp2p-identity", + "parking_lot", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-identify" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "libp2p-identity" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" +dependencies = [ + "bs58", + "ed25519-dalek", + "hkdf", + "multihash", + "prost", + "rand 0.8.6", + "sha2", + "thiserror 2.0.18", + "tracing", + "zeroize", +] + +[[package]] +name = "libp2p-kad" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d3fd632a5872ec804d37e7413ceea20588f69d027a0fa3c46f82574f4dee60" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tracing", + "uint", + "web-time", +] + +[[package]] +name = "libp2p-mdns" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +dependencies = [ + "futures", + "hickory-proto", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-metrics" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-dcutr", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-relay", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-noise" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core", + "libp2p-identity", + "multiaddr", + "multihash", + "quick-protobuf", + "rand 0.8.6", + "snow", + "static_assertions", + "thiserror 2.0.18", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "libp2p-quic" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "quinn", + "rand 0.8.6", + "ring", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-relay" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9b0392ed623243ad298326b9f806d51191829ac7585cc825c54c6c67b04d9" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.6", + "static_assertions", + "thiserror 2.0.18", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-request-response" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f1cca83488b90102abac7b67d5c36fc65bc02ed47620228af7ed002e6a1478" +dependencies = [ + "async-trait", + "futures", + "futures-bounded", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-stream" +version = "0.4.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6bd8025c80205ec2810cfb28b02f362ab48a01bee32c50ab5f12761e033464" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.6", + "tracing", +] + +[[package]] +name = "libp2p-swarm" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "hashlink", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "multistream-select", + "rand 0.8.6", + "smallvec", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +dependencies = [ + "heck", + "quote", + "syn", +] + +[[package]] +name = "libp2p-tcp" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "socket2 0.6.4", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring", + "rustls", + "rustls-webpki", + "thiserror 2.0.18", + "x509-parser", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +dependencies = [ + "futures", + "futures-timer", + "igd-next", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-yamux" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +dependencies = [ + "either", + "futures", + "libp2p-core", + "thiserror 2.0.18", + "tracing", + "yamux 0.12.1", + "yamux 0.13.10", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.12.1", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "llimphi-clipboard" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "arboard", + "llimphi-widget-text-editor", +] + +[[package]] +name = "llimphi-compositor" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-layout", + "llimphi-text", + "vello", + "wgpu", +] + +[[package]] +name = "llimphi-hal" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "pollster", + "raw-window-handle", + "wgpu", + "winit", +] + +[[package]] +name = "llimphi-layout" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "taffy", +] + +[[package]] +name = "llimphi-motion" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", +] + +[[package]] +name = "llimphi-raster" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-hal", + "pollster", + "vello", +] + +[[package]] +name = "llimphi-text" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "parley", + "vello", +] + +[[package]] +name = "llimphi-theme" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-raster", +] + +[[package]] +name = "llimphi-ui" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-compositor", + "llimphi-hal", + "llimphi-layout", + "llimphi-raster", + "llimphi-text", + "pollster", +] + +[[package]] +name = "llimphi-widget-button" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", +] + +[[package]] +name = "llimphi-widget-context-menu" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-panel", +] + +[[package]] +name = "llimphi-widget-edit-menu" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-context-menu", + "llimphi-widget-text-editor", +] + +[[package]] +name = "llimphi-widget-list" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", +] + +[[package]] +name = "llimphi-widget-menubar" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "app-bus", + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-button", + "llimphi-widget-context-menu", +] + +[[package]] +name = "llimphi-widget-panel" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", +] + +[[package]] +name = "llimphi-widget-text-editor" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-text-editor-core", + "tree-sitter", +] + +[[package]] +name = "llimphi-widget-text-editor-core" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "peniko", + "ropey", + "tree-sitter", + "tree-sitter-python", + "tree-sitter-rust", +] + +[[package]] +name = "llimphi-widget-text-input" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", + "llimphi-widget-text-editor", +] + +[[package]] +name = "llimphi-widget-tiled" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "llimphi-theme", + "llimphi-ui", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.12.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint 0.8.0", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" +dependencies = [ + "base-x", + "base256emoji", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" +dependencies = [ + "unsigned-varint 0.8.0", +] + +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.12.1", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum", + "termcolor", + "thiserror 2.0.18", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.12.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.12.1", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.12.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.12.1", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.12.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.12.1", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parley" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28dadbe655332fd7d996794ec8d0c376695f6ca47bc75aa01e0967c7f28e42a" +dependencies = [ + "fontique", + "hashbrown 0.15.5", + "peniko", + "skrifa 0.31.3", + "swash", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "peniko" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b44f9ddd2f480176b34278eb653ec1c8062f3b143a4e16eeff5ffac3334e288" +dependencies = [ + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.12.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "read-fonts" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" +dependencies = [ + "bytemuck", + "font-types 0.9.0", +] + +[[package]] +name = "read-fonts" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ea612a55c08586a1d15134be8a776186c440c312ebda3b9e8efbfe4255b7f4" +dependencies = [ + "bytemuck", + "font-types 0.9.0", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rimay-verbo" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "rimay-verbo-core", + "rimay-verbo-daemon", + "rimay-verbo-mock", +] + +[[package]] +name = "rimay-verbo-core" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "async-trait", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "rimay-verbo-daemon" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "async-trait", + "postcard", + "rimay-verbo-core", + "serde", + "tokio", +] + +[[package]] +name = "rimay-verbo-mock" +version = "0.1.0" +source = "git+https://gitea.gioser.net/sergio/gioser.git#7a412ae2b60e3be40d8b5a53257dc95006ea9f55" +dependencies = [ + "async-trait", + "rimay-verbo-core", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "skrifa" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" +dependencies = [ + "bytemuck", + "read-fonts 0.29.3", +] + +[[package]] +name = "skrifa" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576e60c7de4bb6a803a0312f9bef17e78cf1e8d25a80e1ade76770d7a0237955" +dependencies = [ + "bytemuck", + "read-fonts 0.33.1", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.12.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "ring", + "rustc_version", + "sha2", + "subtle", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +dependencies = [ + "skrifa 0.40.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.12.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-python" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vello" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3f8a53870a2ee699ce05b738a3f9974c92c35ed4874de86052ac68d214811c" +dependencies = [ + "bytemuck", + "futures-intrusive", + "log", + "peniko", + "png 0.17.16", + "skrifa 0.35.0", + "static_assertions", + "thiserror 2.0.18", + "vello_encoding", + "vello_shaders", + "wgpu", +] + +[[package]] +name = "vello_encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69b0fe94b0ac7e47619c504ee2c377355174f5c46353c46d03fa5f7e435922b" +dependencies = [ + "bytemuck", + "guillotiere", + "peniko", + "skrifa 0.35.0", + "smallvec", +] + +[[package]] +name = "vello_shaders" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ebea426bb2f95b7610bca09178b03d809ede1d3c500a9acf6eca43e8f200be" +dependencies = [ + "bytemuck", + "naga", + "thiserror 2.0.18", + "vello_encoding", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.12.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.12.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.12.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.12.1", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.12.1", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.12.1", + "block", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.12.1", + "js-sys", + "log", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.12.1", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.12.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.12.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yamux" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.8.6", + "static_assertions", +] + +[[package]] +name = "yamux" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" +dependencies = [ + "futures", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.9.4", + "static_assertions", + "web-time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e419512 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,439 @@ +# Cargo.toml raíz STANDALONE de khipu — front-door sobre Llimphi. +# Solo el código de khipu; Llimphi y lo fundacional por git-dep del monorepo gioser.git. +[workspace] +resolver = "2" +members = [ + "00_unanchay/khipu/khipu-app", + "00_unanchay/khipu/khipu-brahman", + "00_unanchay/khipu/khipu-core", + "00_unanchay/khipu/khipu-gravity", + "00_unanchay/khipu/khipu-share", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.80" +license = "MIT" +authors = ["Sergio "] +publish = false +repository = "https://gitea.gioser.net/sergio/khipu" + +[workspace.dependencies] + +# === Registro de apps / menú global === +app-bus = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# === Serialización === +serde = { version = "1", features = ["derive"] } +serde_json = "1" +lsp-types = "0.97" +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", "rng-getrandom"] } +sha2 = "0.10" +blake3 = "1.5" +ed25519-dalek = "2" +aes-gcm = "0.10" +chacha20poly1305 = "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"] } + +# === Ingesta de documentos (iniy-ingest: PDF / EPUB) === +pdf-extract = "0.7" +epub = "2.1" + +# === Bulk import Wikipedia (iniy-wiki dump) === +bzip2 = "0.4" + +# === Compresión (minga multi-bundle) === +zstd = "0.13" + +# === HTTP server (iniy-server) === +axum = "0.7" +tower = "0.5" + +# === ANN sobre embeddings (iniy nli --ann) === +instant-distance = "0.6" + +# === P2P (minga) === +libp2p = { version = "0.56", features = ["tokio", "tcp", "noise", "yamux", "macros", "kad", "identify", "relay", "dcutr", "autonat", "mdns"] } +libp2p-stream = "=0.4.0-alpha" +libp2p-allow-block-list = "0.6" + +# === SSH (ssh, sandokan RemoteEngine, matilda) === +russh = "0.54" + +# === Math determinista cross-platform (dominium) === +libm = "0.2" + +# === SMF (takiy-midi) === +# midly: parser/emitter SMF tipo 0/1, no_std-friendly, sin allocs en hot path. +midly = "0.5" + +# === Code parsing (minga) === +arboard = "3" +ropey = "1.6" +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" + +# === Grafos (iniy, nakui-core ya lo usa directo en 0.6) === +petgraph = "0.6" + +# === Image decoding (nahual-image-viewer-llimphi) === +# default-features = false: nos quedamos con PNG + JPEG + WebP (lossless). +# tullpu-render exporta a las tres; AVIF/TIFF/… los habilitamos si una app +# los pide específicamente. +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } + +# === 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 (auth-core) === +pam = "0.8" + +# === D-Bus (arje compat) === +zbus = { version = "4", default-features = false, features = ["tokio"] } + +# === Tests === +tempfile = "3" + +# === Llimphi (motor gráfico soberano) === +# wgpu sobre Vulkan/Metal/DX12, winit para ventana en dev Linux. +# raw-window-handle 0.6 alinea winit 0.30 con wgpu 24. +# vello 0.5 = rasterizador vectorial sobre wgpu 24. +# taffy 0.9 = motor Flexbox/Grid puro Rust (ya pulled por transitivos, lo alineamos). +# parley 0.2 = shaping/layout de texto compatible con peniko 0.4 (que vello 0.5 expone). +wgpu = "24" +winit = "0.30" +raw-window-handle = "0.6" +pollster = "0.4" +vello = "0.5" +taffy = "0.9" +# parley = shaping completo (bidi, ligatures, fallback CJK/emoji vía fontique, line break). +parley = "0.4" +# Bucle Elm (input→update→view→layout→raster→present). Lo consumen las apps. +llimphi-ui = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Paleta semántica compartida por las apps y los widgets. +llimphi-theme = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Tweens y helpers de animación sobre el bucle Elm. +llimphi-motion = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Iconos vectoriales (BezPath en grid 24×24) compartidos por todas las apps. +llimphi-icons = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Widgets reusables sobre llimphi-ui — uno por crate. +llimphi-widget-app-header = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-banner = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-button = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-card = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-clipboard = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-context-menu = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-edit-menu = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-menubar = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-list = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-grid = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-slider = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-scroll = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-splitter = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-stat-card = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-tabs = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-command-palette = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-diff-viewer = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-fif = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-file-picker = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-bookmarks = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-mini-map = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-shuma-term = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-module-symbol-outline = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-plugin-host = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-theme-switcher = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-text-area = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-text-editor-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-text-editor = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-text-editor-lsp = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-text-input = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-tiled = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-nodegraph = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-tree = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-navigator = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Sello vectorial wawa (rombo + W implícita + Merkle Core). +llimphi-widget-wawa-mark = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Widgets de elegancia transversal (tooltip, spinner, progress, toast, +# modal, empty, status-bar, shortcuts-help, splash). +llimphi-widget-tooltip = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-spinner = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-progress = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-toast = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-modal = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-empty = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-status-bar = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-shortcuts-help = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-timeline = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-splash = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Controles de formulario y signaling (switch, segmented, breadcrumb, +# badge, avatar, skeleton, field). +llimphi-widget-switch = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-segmented = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-dock-rail = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-breadcrumb = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-badge = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-avatar = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-skeleton = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-field = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Firma visual transversal (gradient sutil + hairline accent). +llimphi-widget-panel = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-widget-panes = { git = "https://gitea.gioser.net/sergio/gioser.git" } +llimphi-workspace = { git = "https://gitea.gioser.net/sergio/gioser.git" } +# Abstracción Selector — host (paths) + wawa (khipus). +llimphi-module-selector = { git = "https://gitea.gioser.net/sergio/gioser.git" } + +# === Filesystem helpers === +directories = "5" + +# === Diff line-based (llimphi-module-diff-viewer) === +# `similar` es la crate de facto: implementa Myers + Patience + LCS, +# expone `TextDiff` con ChangeTag por línea (Equal/Insert/Delete), +# zero deps fuera de std. La 2.x es estable hace años. +similar = "2" + +# === Fuzzy matching (shuma-history) === +# nucleo-matcher = mismo matcher que helix-editor: rápido, Unicode-correct, +# bonus por prefijos, ranking estable. La versión 0.3 expone el API simple +# que necesitamos (Matcher + Pattern + score). +nucleo-matcher = "0.3" + +# === Transporte autenticado (shuma-link) === +# snow = framework Noise pure-rust. Lo usamos en modo Noise_XK (cliente +# conoce la pubkey del servidor, server descubre la del cliente y la +# valida contra una allowlist). ChaCha20-Poly1305 + X25519 + BLAKE2s. +# La versión 0.9 viene pinneada por libp2p, así nos alineamos. +snow = "0.9" +hex = "0.4" + +# === PTY + emulador de terminal (shuma-exec, módulos REPL) === +# portable-pty aloja un PTY cross-platform; lo usamos para los +# comandos TUI tipo vim/htop/less que necesitan un terminal de verdad. +# vt100 parsea la secuencia de bytes que el PTY emite (ANSI + cursor +# movement + erase + screen state) y mantiene un buffer de pantalla +# renderizable como grid. +portable-pty = "0.9" +vt100 = "0.16" + +# === 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"] } + +# === Archivos comprimidos (nahual archive viewer) === +# Sólo listamos el directorio central (nombres/tamaños); no descomprimimos, +# por eso default-features=false alcanza para ZIP. Para tar.gz sí +# descomprimimos en streaming con flate2 (ya declarado arriba), saltando +# los datos de cada entrada — sólo leemos headers. +zip = { version = "2.4", default-features = false } +tar = { version = "0.4", default-features = false } + +# === Fuentes (nahual font viewer) === +# Parseo de TTF/OTF/TTC y extracción de contornos de glifo a paths. +ttf-parser = "0.25" + +# ============================================================ +# Intra-workspace deps de nahual (referenciadas por workspace = true) +# ============================================================ +nahual-text-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-image-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-thumb-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-gallery-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-video-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-card-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-audio-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-tree-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-hex-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-table-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-markdown-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-archive-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-font-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-map-viewer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-geo-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-viewer-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +nahual-file-explorer-llimphi = { git = "https://gitea.gioser.net/sergio/gioser.git" } + +# ============================================================ +# Intra-workspace deps de pineal (módulo de gráficos) +# ============================================================ +pineal-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-render = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-cartesian = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-stream = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-mesh = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-financial = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-polar = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-heatmap = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-treemap = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-flow = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-phosphor = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-export = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-hexbin = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-contour = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal-bars = { git = "https://gitea.gioser.net/sergio/gioser.git" } +pineal = { git = "https://gitea.gioser.net/sergio/gioser.git" } + +# ============================================================ +# Intra-workspace deps de iniy (laboratorio semántico de creencias) +# ============================================================ +iniy-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-ingest = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-extract = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-nli = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-nli-llm = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-graph = { git = "https://gitea.gioser.net/sergio/gioser.git" } +iniy-store = { git = "https://gitea.gioser.net/sergio/gioser.git" } + +# === auto: declarados por crates internos faltantes === +cosmos-coords = { git = "https://gitea.gioser.net/sergio/gioser.git" } +cosmos-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +cosmos-ephemeris = { git = "https://gitea.gioser.net/sergio/gioser.git" } +cosmos-time = { git = "https://gitea.gioser.net/sergio/gioser.git" } +cosmos-wcs = { git = "https://gitea.gioser.net/sergio/gioser.git" } + +# === auto: externas de eternal === +celestial-eop-data = { version = "0.1"} +approx = "0.5" +byteorder = "1.5" +cc = "1.0" +chrono = "0.4" +crc32fast = "1.4" +criterion = "0.5" +csv = "1.4" +flate2 = "1.0" +glob = "0.3" +indicatif = "0.18" +lz4_flex = "0.11" +memmap2 = "0.9" +mockito = "1.0" +ndarray = "0.15" +num-traits = "0.2" +once_cell = "1.19" +parking_lot = "0.12" +png = "0.18" +proptest = "1.4" +quick-xml = "0.31" +rayon = "1.8" +regex = "1.11" +reqwest = "0.12" +tiff = "0.11" +wide = "0.7" +wiremock = "0.6" + +# === i18n (rimay-localize) === +fluent-bundle = "0.15" +unic-langid = { version = "0.9", features = ["macros"] } +sys-locale = "0.3" + +# === Servo (puriy-engine) === +# Crates publicados de Servo embebibles individualmente. html5ever/markup5ever +# ya entran via ammonia→surrealdb→nakui, así que alineamos versión para no +# duplicar el árbol. markup5ever_rcdom es el DOM Rc-based simple (suficiente +# para Fase 2: parsear y renderizar, sin scripting). cssparser es el tokenizer +# CSS de Stylo, sirve para inline styles. ureq = HTTP síncrono minimalista, +# evita pull de tokio en el engine. +html5ever = "0.39" +markup5ever = "0.39" +markup5ever_rcdom = "0.39" +cssparser = "0.35" +url = "2" +ureq = { version = "2", default-features = false, features = ["tls"] } + +# === takiy-synth (SoundFont MIDI) === +# rustysynth = sintetizador SF2 puro Rust, MIT. Reemplaza el oscilador +# feo de takiy-synth por muestras reales (FluidR3, GeneralUser GS, etc). +rustysynth = "1.3" + +# === takiy-playback (audio device output) === +# cpal = backend de audio cross-platform (ALSA/PulseAudio/Pipewire en +# Linux, WASAPI en Windows, CoreAudio en macOS). Lo usamos sólo para +# abrir el device default y empujar muestras f32 — nada de mezclado +# ni efectos en el callback. +cpal = "0.15" + +# === media-source-wav (decoder PCM en disco) === +# hound = lector/escritor WAV puro-Rust, sin deps nativas. Soporta PCM +# entero (8/16/24/32) y float (32). Suficiente para abrir samples y +# stems de prueba sin meter ffmpeg/symphonia. +hound = "3.5" + +# === media-source-{mp3,flac,vorbis} (decoders vía symphonia) === +# symphonia es una colección de decoders puro-Rust mantenida. `mp3` cubre +# media-source-mp3; `flac` (decoder + demuxer FLAC nativo) cubre +# media-source-flac (lossless); `vorbis` + `ogg` (codec + demuxer Ogg) +# cubren media-source-vorbis (lossy clásico, libre de patentes). Sin aac: +# ese tier patentado entra por shared/foreign-av. +symphonia = { version = "0.5", default-features = false, features = ["mp3", "flac", "vorbis", "ogg"] } + +# === media-source-opus (decoder Opus NATIVO puro-Rust) === +# Opus es el formato de audio nativo de gioser (par del video AV1). ogg +# demuxea las páginas Ogg; opus-wave es un port puro-Rust de libopus +# (SILK+CELT, sin C ni FFI) — par del rav1d del lado video. +ogg = "0.9" +opus-wave = "3" + +# === media-source-webm (demux nativo Matroska/WebM) === +# matroska-demuxer es un demuxer puro-Rust de MKV/WebM (EBML). Saca los +# paquetes de los tracks V_AV1 y A_OPUS para alimentar a media-source-av1 +# y media-source-opus — un .webm AV1+Opus se reproduce 100% nativo. +matroska-demuxer = "0.7" +# === git-deps al monorepo (agregados por la extracción) === +agora-core = { git = "https://gitea.gioser.net/sergio/gioser.git" } +agora-keystore = { git = "https://gitea.gioser.net/sergio/gioser.git" } +card-net = { git = "https://gitea.gioser.net/sergio/gioser.git" } +rimay-verbo = { git = "https://gitea.gioser.net/sergio/gioser.git" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ede9631 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sergio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d9dc97 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# khipu + +> Sovereign P2P "notes to oblivion" — local-first, content-addressed, in Rust. + +`khipu` (Quechua: *knotted-cord record*) is a sovereign note system: local-first, content-addressed, synced peer-to-peer over the `card`/`chasqui` layer (LAN + WAN + relay/NAT traversal, UDP & DHT discovery, encrypted identity). Notes are designed to be let go of as much as kept — a record that forgets on purpose. + +## How dependencies work +Front-door repo: only `khipu-*` crates here. Identity (`card`), networking (`chasqui`) and shared leaves are git-dependencies of the [`gioser`](https://gitea.gioser.net/sergio/gioser) monorepo. + +## License +MIT. Part of the [gioser](https://gitea.gioser.net/sergio/gioser) suite.