diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa0d4dcc..07abdaf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,3 +165,40 @@ jobs: - name: cargo test backend working-directory: gui/src-tauri run: cargo test + + # The mobile (iOS/Android) Tauri shell. It reuses the same Svelte frontend + # as the desktop and embeds the phone's viewer/controller core + # (allmystuff-mobile-core) instead of spawning a desktop node, so it never + # pulls the GTK/WebKitGTK desktop webview — on device Tauri uses the system + # webview. We verify it type-checks for a real mobile target. No Android NDK + # is needed yet: this shell links no C-building crate (the embedded + # myownmesh-core engine, which brings `ring`, lands in a later slice). + gui-mobile: + name: GUI mobile · android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-linux-android + - uses: Swatinem/rust-cache@v2 + with: + workspaces: gui/mobile + # tauri's build script / generate_context! want the frontend dist to + # exist — it's the same build the desktop shell consumes. + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: gui/pnpm-lock.yaml + - name: build frontend (for tauri build script) + working-directory: gui + run: | + pnpm install --frozen-lockfile + pnpm build + - name: cargo check mobile shell (android target) + working-directory: gui/mobile + run: cargo check --target aarch64-linux-android diff --git a/Cargo.lock b/Cargo.lock index c7d0609b..7ba4ae23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,19 @@ dependencies = [ "sysinfo", ] +[[package]] +name = "allmystuff-mobile-core" +version = "0.2.4" +dependencies = [ + "allmystuff-graph", + "allmystuff-protocol", + "allmystuff-session", + "base64", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "allmystuff-protocol" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 2e9585b6..d29605c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/allmystuff-service", "crates/allmystuff-cli", "crates/allmystuff-term", + "crates/allmystuff-mobile-core", ] # The Tauri desktop app (`gui/src-tauri`) and the headless mesh node # (`node/` — the `allmystuff-node` engine + the `allmystuff-serve` binary) @@ -38,6 +39,7 @@ allmystuff-bridge = { path = "crates/allmystuff-bridge", version = "0.2.4" } allmystuff-session = { path = "crates/allmystuff-session", version = "0.2.4" } allmystuff-updater = { path = "crates/allmystuff-updater", version = "0.2.4" } allmystuff-service = { path = "crates/allmystuff-service", version = "0.2.4" } +allmystuff-mobile-core = { path = "crates/allmystuff-mobile-core", version = "0.2.4" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/README.md b/README.md index 30f223c1..167319eb 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ AllMyStuff is under active development. What works today: | Audio and camera | ✅ | Opus audio and H.264 camera over the same transport | | Discovery and routing | ✅ | Peers appear via presence; sessions negotiate and tear down cleanly | | File/disk streaming | 🚧 | Sessions set up; bulk transport is in progress | +| Mobile (iOS/Android) | 🚧 | In design; the pure client core has landed (`allmystuff-mobile-core`). See [docs/MOBILE.md](docs/MOBILE.md) | ## Building from source diff --git a/crates/allmystuff-mobile-core/Cargo.toml b/crates/allmystuff-mobile-core/Cargo.toml new file mode 100644 index 00000000..7b62b3c4 --- /dev/null +++ b/crates/allmystuff-mobile-core/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "allmystuff-mobile-core" +description = "The transport-agnostic core of the AllMyStuff mobile (iOS/Android) client: the phone's viewer/controller capability model, the client-side media decode/encode planes, route negotiation helpers, and the MeshClient seam the embedded myownmesh-core fills in. Pure Rust — builds and tests with nothing but serde, so the wire contract is verified the same way the desktop crates are." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[lints.rust] +unsafe_code = "forbid" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +allmystuff-graph = { workspace = true } +allmystuff-protocol = { workspace = true } +allmystuff-session = { workspace = true } + +[dev-dependencies] +# Tests build media-channel frames (whose byte fields travel base64 in JSON) +# straight from JSON values, since the frame tag types are private to +# allmystuff-session. Same engine the wire uses, so the fixtures are honest. +base64 = { workspace = true } diff --git a/crates/allmystuff-mobile-core/src/caps.rs b/crates/allmystuff-mobile-core/src/caps.rs new file mode 100644 index 00000000..eb7534aa --- /dev/null +++ b/crates/allmystuff-mobile-core/src/caps.rs @@ -0,0 +1,258 @@ +//! The capabilities a phone advertises on the graph. +//! +//! This is the mobile counterpart to `allmystuff-bridge`'s +//! `capabilities_from_inventory`: where a desktop scans real hardware and +//! exposes a screen, a control sink, mics, monitors and so on, a phone has a +//! small, fixed, *synthetic* set that says "I am a thing you look through and +//! reach out with." The ids follow the same `:` scheme the +//! bridge uses so a remote machine's [`Catalog`](allmystuff_graph::Catalog) +//! reasons about a phone exactly like any other node. +//! +//! The base (viewer / controller) set, on every phone: +//! +//! | id | media | flow | origin | why | +//! |---|---|---|---|---| +//! | `:display-in` | Display | Sink | `remote-desktop` | a remote **screen** (RDC) lands here | +//! | `:video-in` | Video | Sink | `viewer` | a remote **camera** lands here | +//! | `:audio-out` | Audio | Sink | `speaker` | remote audio plays here | +//! | `:keyboard-mouse` | Input | Source | `controller` | touch drives a remote | +//! | `:clipboard` | Clipboard | Duplex | `clipboard` | cross-device copy/paste | +//! +//! The split between `display-in` and `video-in` is not cosmetic: the graph +//! routes a remote *screen* (`MediaKind::Display`) only to a display sink and +//! a remote *camera* (`MediaKind::Video`) only to a video sink — the same way +//! the desktop lands a remote screen on its physical monitor but a camera on +//! its synthetic `video-in`. A phone has no monitor to expose, so it exposes a +//! synthetic display sink that *means* "show a remote desktop on my screen." +//! +//! The host set ([`MobileScope::ViewerControllerHost`]) additionally lets the +//! phone be a *source* — its camera, its mic, and (where the platform allows) +//! its own screen. That pulls real capture work onto the device +//! (AVFoundation / Camera2 / ReplayKit / MediaProjection) and is therefore an +//! explicit opt-in, never the default. + +use allmystuff_graph::{Capability, Flow, MediaKind, NodeId}; +use allmystuff_protocol::{ + FEATURE_CAMERA, FEATURE_FILES, FEATURE_MEDIA_LANES, FEATURE_ROOMS, FEATURE_TERMINAL, +}; + +/// How much of itself a phone is willing to expose. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MobileScope { + /// The default and the only scope v1–v2 ship: the phone consumes (a + /// remote screen, remote audio) and reaches out (drives input, opens a + /// shell, browses files). It hosts nothing. + ViewerController, + /// Additionally offer the phone's own camera, microphone, and screen as + /// *sources* to the fleet. Opt-in, because it pulls platform capture into + /// the app. Builds on top of the viewer/controller set, never replaces it. + ViewerControllerHost, +} + +impl MobileScope { + /// Does this scope let the phone act as a media *source*? + pub fn is_host(self) -> bool { + matches!(self, MobileScope::ViewerControllerHost) + } +} + +/// Build every capability a phone `node` should expose for `scope`. +/// +/// The base set comes first and in a stable order (so a remote's +/// `match_endpoint` tie-breaks deterministically), then the host additions. +pub fn mobile_capabilities(node: &NodeId, scope: MobileScope) -> Vec { + let n = node.as_str(); + let mk = |id: String, label: &str, media, flow, origin: &str| { + Capability::new(node.clone(), id, label, media, flow, origin.to_string()) + }; + + let mut caps = vec![ + // Where a remote machine's *desktop* lands — the remote-control case. + // `Display` media, so the graph wires a remote `screen` (Display + // source) straight to it, exactly as the desktop lands one on its + // physical monitor. A phone has no monitor to expose, so this synthetic + // sink stands in for "render a remote desktop on my screen." + mk( + format!("{n}:display-in"), + "Remote desktop", + MediaKind::Display, + Flow::Sink, + "remote-desktop", + ), + // Where a remote *camera* lands. Same role as the desktop's synthetic + // `video-in` — `Video` media, distinct from `Display` so a webcam feed + // and a desktop stream never cross-wire. + mk( + format!("{n}:video-in"), + "Camera viewer", + MediaKind::Video, + Flow::Sink, + "viewer", + ), + // Where a remote machine's audio plays. The desktop folds this into a + // duplex `system-audio` endpoint; a phone has nothing to capture in + // that sense at viewer scope, so it advertises a plain sink. + mk( + format!("{n}:audio-out"), + "Audio out", + MediaKind::Audio, + Flow::Sink, + "speaker", + ), + // The outbound twin: the phone driving a remote. Mirrors the desktop's + // `keyboard-mouse` *source* (origin `controller`) — what a console + // forwards is "whatever this device's user does," which on a phone is + // taps, drags, and the on-screen keyboard. + mk( + format!("{n}:keyboard-mouse"), + "Touch control", + MediaKind::Input, + Flow::Source, + "controller", + ), + // Cross-device clipboard. Duplex like the desktop's: the phone can both + // push a copied value to a remote and pull one back. + mk( + format!("{n}:clipboard"), + "Clipboard", + MediaKind::Clipboard, + Flow::Duplex, + "clipboard", + ), + ]; + + if scope.is_host() { + caps.push(mk( + format!("{n}:camera"), + "Camera", + MediaKind::Video, + Flow::Source, + "camera", + )); + caps.push(mk( + format!("{n}:microphone"), + "Microphone", + MediaKind::Audio, + Flow::Source, + "microphone", + )); + // The phone's own screen as a remote display, for "show my phone on the + // big screen." Platform-gated (ReplayKit / MediaProjection); only here + // at host scope. + caps.push(mk( + format!("{n}:screen"), + "Phone screen", + MediaKind::Display, + Flow::Source, + "screen", + )); + } + + caps +} + +/// The AllMyStuff feature tags a phone advertises in its +/// [`NodeProfile::features`](allmystuff_protocol::NodeProfile). A feature is +/// only ever offered to a peer that also advertises it, so listing one the +/// phone can drive (terminal, files, rooms) is what lets the *remote* offer it +/// back. `media-lanes` opts the phone into the RTP H.264/Opus track lanes +/// (the path the native decoder consumes). `camera` is host-scope only — the +/// phone won't claim it can source a camera unless it actually will. +pub fn mobile_features(scope: MobileScope) -> Vec { + let mut f = vec![ + FEATURE_TERMINAL.to_string(), + FEATURE_FILES.to_string(), + FEATURE_ROOMS.to_string(), + FEATURE_MEDIA_LANES.to_string(), + ]; + if scope.is_host() { + f.push(FEATURE_CAMERA.to_string()); + } + f +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ids(caps: &[Capability]) -> Vec { + caps.iter().map(|c| c.id.as_str().to_string()).collect() + } + + #[test] + fn viewer_controller_is_consumer_and_controller_only() { + let me = NodeId::from("phone-1"); + let caps = mobile_capabilities(&me, MobileScope::ViewerController); + let by_origin = |o: &str| caps.iter().find(|c| c.origin == o).cloned(); + + let viewer = by_origin("viewer").expect("video-in"); + assert_eq!((viewer.media, viewer.flow), (MediaKind::Video, Flow::Sink)); + assert_eq!(viewer.id.as_str(), "phone-1:video-in"); + + // The remote-desktop sink is Display, not Video — that's what lets a + // remote screen land on the phone at all. + let rdc = by_origin("remote-desktop").expect("display-in"); + assert_eq!((rdc.media, rdc.flow), (MediaKind::Display, Flow::Sink)); + assert_eq!(rdc.id.as_str(), "phone-1:display-in"); + + let ctl = by_origin("controller").expect("keyboard-mouse"); + assert_eq!((ctl.media, ctl.flow), (MediaKind::Input, Flow::Source)); + assert_eq!(ctl.id.as_str(), "phone-1:keyboard-mouse"); + + let spk = by_origin("speaker").expect("audio-out"); + assert_eq!((spk.media, spk.flow), (MediaKind::Audio, Flow::Sink)); + + let clip = by_origin("clipboard").expect("clipboard"); + assert_eq!( + (clip.media, clip.flow), + (MediaKind::Clipboard, Flow::Duplex) + ); + + // A viewer phone never *hosts* a screen, camera, or mic: nothing it + // exposes can be the `from` side of a video/display/audio route. (It + // can still source input and clipboard — touch drives a remote, and + // the clipboard is duplex.) + assert!( + caps.iter() + .filter(|c| c.flow.can_source()) + .all(|c| !matches!( + c.media, + MediaKind::Video | MediaKind::Display | MediaKind::Audio + )), + "a viewer phone must not source video/display/audio" + ); + } + + #[test] + fn host_scope_adds_sources_on_top_of_the_base_set() { + let me = NodeId::from("phone-1"); + let base = mobile_capabilities(&me, MobileScope::ViewerController); + let host = mobile_capabilities(&me, MobileScope::ViewerControllerHost); + + // Host is a strict superset: every base id is still present. + for id in ids(&base) { + assert!(ids(&host).contains(&id), "host dropped base cap {id}"); + } + + let cam = host.iter().find(|c| c.origin == "camera").expect("camera"); + assert_eq!((cam.media, cam.flow), (MediaKind::Video, Flow::Source)); + assert!(host.iter().any(|c| c.origin == "screen")); + assert!(host.iter().any(|c| c.origin == "microphone")); + // ...and none of those exist at viewer scope. + assert!(base.iter().all(|c| c.origin != "camera")); + } + + #[test] + fn features_gate_camera_behind_host_scope() { + assert!( + !mobile_features(MobileScope::ViewerController).contains(&FEATURE_CAMERA.to_string()) + ); + assert!(mobile_features(MobileScope::ViewerControllerHost) + .contains(&FEATURE_CAMERA.to_string())); + // Terminal + files are always offered — the phone can drive both. + assert!( + mobile_features(MobileScope::ViewerController).contains(&FEATURE_TERMINAL.to_string()) + ); + assert!(mobile_features(MobileScope::ViewerController).contains(&FEATURE_FILES.to_string())); + } +} diff --git a/crates/allmystuff-mobile-core/src/connect.rs b/crates/allmystuff-mobile-core/src/connect.rs new file mode 100644 index 00000000..6ed2ad66 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/connect.rs @@ -0,0 +1,358 @@ +//! Building the [`RouteControl::Offer`] a phone sends to start a session. +//! +//! Two shapes of route exist, and they are negotiated differently: +//! +//! * **Catalog routes** — screen (`display`), camera (`video`), audio. These +//! wire one advertised [`Capability`] to another and are validated + +//! authorized through the receiver-side [`Catalog`] +//! ([`Catalog::propose_route`]). [`offer_route`] and the named helpers +//! ([`offer_screen`], [`offer_camera`], [`offer_audio`]) take this path. +//! +//! * **Synthetic feature routes** — terminal and files. These don't match +//! advertised capabilities; the host recognizes them by id pattern +//! (`:terminal`, `:files`) and authorizes them by *fleet +//! membership*, not by a grant. [`offer_terminal`] / [`offer_files`] build +//! them directly, with `media: "generic"` — exactly what `amst` and the +//! desktop GUI send. +//! +//! Either way the route id is the value both ends independently derive, +//! `route:{from}→{to}` (see [`route_id`]), so the offer the phone sends names +//! the same route the host will key its side on. + +use allmystuff_graph::{Capability, CapabilityId, Catalog, GrantRole, MediaKind, NodeId, Route}; +use allmystuff_protocol::{ControlMessage, RouteControl}; + +/// The codecs a phone advertises it can *consume* on a display/camera route, +/// best first. The host picks the best it can produce, falling back to MJPEG +/// over the media channel when the list is empty or nothing matches — the +/// same degradation contract the desktop uses. +pub const VIDEO_CODECS: &[&str] = &["h264"]; +/// The audio codecs a phone can consume, best first; empty/none ⇒ PCM frames. +pub const AUDIO_CODECS: &[&str] = &["opus"]; + +/// What can go wrong assembling an offer. +#[derive(Debug, thiserror::Error)] +pub enum ConnectError { + /// The catalog has no endpoint of the needed media+role on that node — + /// e.g. the remote advertises no screen, or the phone's own sink is + /// missing from the catalog the caller built. + #[error("no {role} endpoint for {media} on node {node}")] + NoEndpoint { + node: String, + media: &'static str, + role: &'static str, + }, + /// The route failed the catalog's media/flow/authorization checks. + #[error(transparent)] + Rejected(#[from] allmystuff_graph::ConnectError), +} + +/// The route id both ends derive for a `from`→`to` pair. Matches +/// `allmystuff_graph`'s private `route_id` and the node's own +/// `format!("route:{from}→{to}")` byte-for-byte. +pub fn route_id(from: &str, to: &str) -> String { + format!("route:{from}→{to}") +} + +/// Offer a **catalog-validated** route between two advertised capabilities, +/// carrying the codecs the phone can consume. The route is built and +/// authorized through `catalog` (so an unauthorized or media-mismatched +/// connection is refused here, before anything hits the wire) and wrapped in +/// a [`ControlMessage::Route`] ready to publish on the control channel. +pub fn offer_route( + catalog: &Catalog, + from: &CapabilityId, + to: &CapabilityId, + video: Vec, + audio: Vec, +) -> Result { + let route = catalog.propose_route(from, to)?; + Ok(ControlMessage::Route(RouteControl::Offer { + route, + video, + audio, + session: None, + })) +} + +/// Watch a remote machine's **desktop** on this phone: the remote's screen +/// (a `Display` source) → the phone's `display-in` (a `Display` sink), +/// offering H.264 with the usual MJPEG fallback. +/// +/// When the remote exposes several screens, this picks its default/first via +/// [`Catalog::match_endpoint`]; to target a specific monitor +/// (`:screen:`), look the capability up yourself and call +/// [`offer_route`]. +pub fn offer_screen( + catalog: &Catalog, + remote: &NodeId, + me: &NodeId, +) -> Result { + let from = endpoint(catalog, remote, MediaKind::Display, GrantRole::Provide)?; + let to = endpoint(catalog, me, MediaKind::Display, GrantRole::Consume)?; + offer_route(catalog, &from, &to, codecs(VIDEO_CODECS), Vec::new()) +} + +/// View a remote machine's **camera** on this phone: a `Video` source → the +/// phone's `video-in` (a `Video` sink). +pub fn offer_camera( + catalog: &Catalog, + remote: &NodeId, + me: &NodeId, +) -> Result { + let from = endpoint(catalog, remote, MediaKind::Video, GrantRole::Provide)?; + let to = endpoint(catalog, me, MediaKind::Video, GrantRole::Consume)?; + offer_route(catalog, &from, &to, codecs(VIDEO_CODECS), Vec::new()) +} + +/// Listen to a remote machine's **audio** on this phone: an `Audio` source → +/// the phone's `audio-out` (an `Audio` sink), offering Opus with PCM fallback. +pub fn offer_audio( + catalog: &Catalog, + remote: &NodeId, + me: &NodeId, +) -> Result { + let from = endpoint(catalog, remote, MediaKind::Audio, GrantRole::Provide)?; + let to = endpoint(catalog, me, MediaKind::Audio, GrantRole::Consume)?; + offer_route(catalog, &from, &to, Vec::new(), codecs(AUDIO_CODECS)) +} + +/// Open a **terminal** on `host` from this phone (`me`). +/// +/// `attach` joins an already-running shell by its host-side session id +/// (tmux-style multi-attach); `None` mints a fresh shell. `nonce` makes the +/// viewer-side endpoint unique across this phone's open terminals — use a +/// per-tab counter or timestamp (this crate has no clock). No catalog: a +/// terminal route is recognized by id and authorized by fleet membership on +/// the host. +pub fn offer_terminal( + host: &NodeId, + me: &NodeId, + attach: Option, + nonce: &str, +) -> ControlMessage { + let from = format!("{}:terminal", host.as_str()); + let to = format!("{}:term-view:{nonce}", me.as_str()); + ControlMessage::Route(RouteControl::Offer { + route: generic_route(from, to), + video: Vec::new(), + audio: Vec::new(), + session: attach, + }) +} + +/// Browse `host`'s **files** from this phone (`me`). Like [`offer_terminal`], +/// a synthetic fleet-gated route; `nonce` keeps concurrent browsers distinct. +pub fn offer_files(host: &NodeId, me: &NodeId, nonce: &str) -> ControlMessage { + let from = format!("{}:files", host.as_str()); + let to = format!("{}:files-view:{nonce}", me.as_str()); + ControlMessage::Route(RouteControl::Offer { + route: generic_route(from, to), + video: Vec::new(), + audio: Vec::new(), + session: None, + }) +} + +/// Tear a route down from this side — either end may. A thin wrapper so the +/// caller doesn't hand-roll the control message. +pub fn teardown(route_id: impl Into) -> ControlMessage { + ControlMessage::Route(RouteControl::Teardown { + route_id: route_id.into(), + }) +} + +// ---- helpers ---------------------------------------------------------- + +fn generic_route(from: String, to: String) -> Route { + Route { + id: route_id(&from, &to), + from: from.into(), + to: to.into(), + media: MediaKind::Generic, + } +} + +fn endpoint( + catalog: &Catalog, + node: &NodeId, + media: MediaKind, + role: GrantRole, +) -> Result { + catalog + .match_endpoint(node, media, role) + .map(|c: &Capability| c.id.clone()) + .ok_or_else(|| ConnectError::NoEndpoint { + node: node.as_str().to_string(), + media: media.token(), + role: role.label(), + }) +} + +fn codecs(list: &[&str]) -> Vec { + list.iter().map(|s| s.to_string()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::caps::{mobile_capabilities, MobileScope}; + use allmystuff_graph::{Flow, MeshNode, NodeKind, Relationship}; + + /// A catalog with the phone (viewer/controller) and a desk PC I own that + /// exposes a screen, a camera, and system audio. + fn fleet_catalog(phone: &NodeId, desk: &NodeId) -> Catalog { + let mut cat = Catalog::new(); + cat.nodes.push(MeshNode { + id: phone.clone(), + label: "Phone".into(), + kind: NodeKind::This, + relationship: Relationship::Mine, + online: true, + }); + cat.nodes.push(MeshNode { + id: desk.clone(), + label: "Desk PC".into(), + kind: NodeKind::Machine, + relationship: Relationship::Mine, + online: true, + }); + for c in mobile_capabilities(phone, MobileScope::ViewerController) { + cat.capabilities.push(c); + } + let d = desk.as_str(); + cat.capabilities.push(Capability::new( + desk.clone(), + format!("{d}:screen"), + "Screen", + MediaKind::Display, + Flow::Source, + "screen", + )); + cat.capabilities.push(Capability::new( + desk.clone(), + format!("{d}:webcam"), + "Webcam", + MediaKind::Video, + Flow::Source, + "camera", + )); + cat.capabilities.push(Capability::new( + desk.clone(), + format!("{d}:system-audio"), + "System audio", + MediaKind::Audio, + Flow::Duplex, + "system", + )); + cat + } + + fn offer(msg: &ControlMessage) -> serde_json::Value { + serde_json::to_value(msg).unwrap() + } + + #[test] + fn screen_offer_wires_remote_display_to_the_phones_display_sink() { + let phone = NodeId::from("phone"); + let desk = NodeId::from("desk"); + let cat = fleet_catalog(&phone, &desk); + + let msg = offer_screen(&cat, &desk, &phone).expect("authorized"); + let j = offer(&msg); + assert_eq!(j["t"], "route"); + assert_eq!(j["kind"], "offer"); + assert_eq!(j["route"]["from"], "desk:screen"); + assert_eq!(j["route"]["to"], "phone:display-in"); + assert_eq!(j["route"]["media"], "display"); + assert_eq!(j["route"]["id"], "route:desk:screen→phone:display-in"); + assert_eq!(j["video"][0], "h264"); + // A display offer carries no audio codec list. + assert!(j["audio"].as_array().map(|a| a.is_empty()).unwrap_or(true)); + } + + #[test] + fn camera_offer_lands_on_video_in_not_display_in() { + let phone = NodeId::from("phone"); + let desk = NodeId::from("desk"); + let cat = fleet_catalog(&phone, &desk); + + let j = offer(&offer_camera(&cat, &desk, &phone).unwrap()); + assert_eq!(j["route"]["from"], "desk:webcam"); + assert_eq!(j["route"]["to"], "phone:video-in"); + assert_eq!(j["route"]["media"], "video"); + } + + #[test] + fn audio_offer_carries_opus() { + let phone = NodeId::from("phone"); + let desk = NodeId::from("desk"); + let cat = fleet_catalog(&phone, &desk); + + let j = offer(&offer_audio(&cat, &desk, &phone).unwrap()); + assert_eq!(j["route"]["media"], "audio"); + assert_eq!(j["route"]["to"], "phone:audio-out"); + assert_eq!(j["audio"][0], "opus"); + } + + #[test] + fn missing_endpoint_is_an_error_not_a_panic() { + let phone = NodeId::from("phone"); + let desk = NodeId::from("desk"); + // A bare catalog: the desk advertises nothing. + let mut cat = Catalog::new(); + for c in mobile_capabilities(&phone, MobileScope::ViewerController) { + cat.capabilities.push(c); + } + let err = offer_screen(&cat, &desk, &phone).unwrap_err(); + assert!(matches!(err, ConnectError::NoEndpoint { .. })); + } + + #[test] + fn terminal_offer_matches_the_amst_wire_shape() { + let host = NodeId::from("desk"); + let me = NodeId::from("phone"); + let j = offer(&offer_terminal(&host, &me, None, "tab-1")); + assert_eq!(j["t"], "route"); + assert_eq!(j["kind"], "offer"); + assert_eq!(j["route"]["from"], "desk:terminal"); + assert_eq!(j["route"]["to"], "phone:term-view:tab-1"); + assert_eq!(j["route"]["media"], "generic"); + assert_eq!( + j["route"]["id"], + "route:desk:terminal→phone:term-view:tab-1" + ); + // A fresh shell omits the session key entirely (skip_serializing_if). + assert!(j.get("session").is_none() || j["session"].is_null()); + } + + #[test] + fn terminal_attach_threads_the_session_id() { + let j = offer(&offer_terminal( + &NodeId::from("desk"), + &NodeId::from("phone"), + Some("term-3".into()), + "tab-2", + )); + assert_eq!(j["session"], "term-3"); + } + + #[test] + fn files_offer_is_a_generic_files_view_route() { + let j = offer(&offer_files( + &NodeId::from("desk"), + &NodeId::from("phone"), + "br-1", + )); + assert_eq!(j["route"]["from"], "desk:files"); + assert_eq!(j["route"]["to"], "phone:files-view:br-1"); + assert_eq!(j["route"]["media"], "generic"); + } + + #[test] + fn teardown_names_the_route() { + let j = offer(&teardown("route:desk:terminal→phone:term-view:tab-1")); + assert_eq!(j["kind"], "teardown"); + assert_eq!(j["route_id"], "route:desk:terminal→phone:term-view:tab-1"); + } +} diff --git a/crates/allmystuff-mobile-core/src/lib.rs b/crates/allmystuff-mobile-core/src/lib.rs new file mode 100644 index 00000000..2100425f --- /dev/null +++ b/crates/allmystuff-mobile-core/src/lib.rs @@ -0,0 +1,94 @@ +//! # allmystuff-mobile-core +//! +//! The brain of the AllMyStuff **mobile** app (iOS / Android), minus +//! everything platform-specific. It is deliberately pure Rust — `serde`, +//! `thiserror`, and the three pure AllMyStuff library crates +//! ([`allmystuff_graph`], [`allmystuff_protocol`], [`allmystuff_session`]) — +//! so the bytes the phone puts on (and takes off) the mesh are verified by +//! `cargo test`, exactly the way the desktop's wire contract is. No webview, +//! no daemon, no native decoders, no network: those live one layer up. +//! +//! ## Why the phone is a *node*, not a thin client +//! +//! On the desktop, the GUI is a thin client of a separate `allmystuff-serve` +//! process it **spawns**, which in turn spawns the `myownmesh` daemon. iOS +//! forbids a sandboxed app from spawning child processes, so that model can't +//! cross over. The mobile app instead **embeds** `myownmesh-core` in-process +//! and becomes a first-class mesh peer: its own ed25519 identity, direct +//! WebRTC DTLS/SRTP to its peers, signaling only. The "no central server, +//! peer-to-peer, end-to-end encrypted" promise is preserved — see +//! `docs/MOBILE.md` for the full architecture and the cross-compile findings. +//! +//! That embedded engine is reached through one seam — [`MeshClient`] — which +//! the FFI layer over `myownmesh-core` implements on device and an in-memory +//! fake implements in tests. Everything in this crate is written against that +//! seam, so the orchestration logic is exercised without a radio. +//! +//! ## What a phone is, on the graph +//! +//! A phone is a **viewer / controller**, not a host (see [`MobileScope`]): +//! +//! * it can **receive** a remote machine's screen/camera ([`MediaKind::Video`] +//! sink) and play its audio, so [`media::VideoSink`] reassembles MJPEG and +//! the [`media::VideoDecoder`] seam feeds the platform H.264 decoder; +//! * it can **drive** a remote ([`MediaKind::Input`] source) — touch becomes +//! normalized [`InputAction`]s via [`media::InputEncoder`]; +//! * it can open a remote **shell** ([`media::TermPlane`]) and browse remote +//! **files** ([`media::FileClient`]); +//! * it does **not** host its own screen, a PTY, or input injection. Those are +//! desktop-only capture concerns with no clean (or, for input, any) mobile +//! story. Phone-as-a-*source* (camera / mic / screen-share) is a later, +//! opt-in [`MobileScope::ViewerControllerHost`] decision. +//! +//! ## Layout +//! +//! * [`caps`] — the phone's [`Capability`] set and advertised feature flags. +//! * [`node`] — assemble the phone's [`NodeProfile`] for presence. +//! * [`connect`] — build the [`RouteControl::Offer`] for a screen / terminal / +//! files / audio route, validated through the receiver-side [`Catalog`]. +//! * [`transport`] — the [`MeshClient`] seam and [`transport::classify`], which +//! turns a raw `(channel, payload)` off the mesh into a typed [`Inbound`]. +//! * [`media`] — the per-plane client pipelines (video, input, terminal, +//! files), all of them pure functions over the [`allmystuff_session`] frame +//! types. + +pub mod caps; +pub mod connect; +pub mod media; +pub mod node; +pub mod transport; + +pub use caps::{mobile_capabilities, MobileScope}; +pub use connect::{offer_files, offer_screen, offer_terminal, ConnectError as OfferError}; +pub use node::{mobile_profile, MobileNodeConfig}; +pub use transport::{classify, Inbound, MeshClient, MeshError, MeshResult}; + +/// A convenient single import for the types a mobile front end touches most. +/// +/// ``` +/// use allmystuff_mobile_core::prelude::*; +/// +/// let me = NodeId::from("phone-abc"); +/// let caps = mobile_capabilities(&me, MobileScope::ViewerController); +/// assert!(caps.iter().any(|c| c.origin == "viewer")); +/// ``` +pub mod prelude { + pub use crate::caps::{mobile_capabilities, MobileScope}; + pub use crate::connect::{offer_files, offer_screen, offer_terminal}; + pub use crate::media::{ + FileClient, FileReply, InputEncoder, TermPlane, VideoDecoder, VideoSink, VideoUpdate, + }; + pub use crate::node::{mobile_profile, MobileNodeConfig}; + pub use crate::transport::{classify, Inbound, MeshClient}; + + pub use allmystuff_graph::{ + Capability, CapabilityId, Catalog, Flow, MediaKind, MeshNode, NodeId, NodeKind, + Relationship, Route, + }; + pub use allmystuff_protocol::{ + ControlMessage, NodeProfile, RouteControl, CHANNEL_CONTROL, CHANNEL_MEDIA, CHANNEL_PRESENCE, + }; + pub use allmystuff_session::{ + FileEvent, InputAction, MediaPayload, TermEvent, VideoFrame, VideoStatusFrame, + }; +} diff --git a/crates/allmystuff-mobile-core/src/media/files.rs b/crates/allmystuff-mobile-core/src/media/files.rs new file mode 100644 index 00000000..8b796d6f --- /dev/null +++ b/crates/allmystuff-mobile-core/src/media/files.rs @@ -0,0 +1,383 @@ +//! The files plane on a phone: a request/reply client over a `files` route. +//! +//! The viewer *asks* ([`FileEvent::List`], [`FileEvent::Read`], +//! [`FileEvent::Write`], [`FileEvent::Mkdir`], [`FileEvent::Rename`], +//! [`FileEvent::Delete`]) and the host *answers* ([`FileEvent::Entries`], +//! [`FileEvent::Chunk`], [`FileEvent::Ok`], [`FileEvent::Err`]). Every request +//! carries a viewer-minted `req` id so replies — especially the multi-`Chunk` +//! body of a `Read` — correlate back. [`FileClient`] owns the `req` counter, +//! builds the request frames, and reassembles in-flight reads into a finished +//! buffer. + +use std::collections::HashMap; + +use allmystuff_session::{FileEntry, FileEvent, FileFrame}; + +/// A reply, once [`FileClient::accept`] has interpreted (and, for reads, +/// accumulated) one inbound host frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FileReply { + /// A directory listing. `home` is the host's home dir (sent on every + /// listing) so the viewer can mark it and start there. + Listing { + req: u64, + path: String, + home: String, + entries: Vec, + }, + /// A read is partway done — `received` of `total` bytes so far. Emitted on + /// every non-final chunk so the UI can show progress. + ReadProgress { req: u64, received: u64, total: u64 }, + /// A read finished: the whole file's bytes, reassembled in order. + ReadDone { req: u64, data: Vec }, + /// A `Write`/`Mkdir`/`Rename`/`Delete` succeeded. + Ok { req: u64 }, + /// The request failed, with the host's reason. + Err { req: u64, reason: String }, +} + +#[derive(Debug, Default)] +struct ReadAccum { + data: Vec, + total: u64, +} + +/// A files client bound to one `files` route. Cheap; one per open browser. +#[derive(Debug)] +pub struct FileClient { + route: String, + seq: u64, + next_req: u64, + reads: HashMap, +} + +impl FileClient { + pub fn new(route: impl Into) -> Self { + FileClient { + route: route.into(), + seq: 0, + // req 0 reads as "no request we track" in the wire types, so start + // at 1 — a real request never collides with that sentinel. + next_req: 1, + reads: HashMap::new(), + } + } + + pub fn route(&self) -> &str { + &self.route + } + + fn alloc(&mut self) -> u64 { + let r = self.next_req; + self.next_req += 1; + r + } + + fn frame(&mut self, event: FileEvent) -> FileFrame { + let seq = self.seq; + self.seq += 1; + FileFrame::new(self.route.clone(), seq, event) + } + + /// List a directory (`""` or `"~"` mean the host's home). + pub fn list(&mut self, path: impl Into) -> (u64, FileFrame) { + let req = self.alloc(); + ( + req, + self.frame(FileEvent::List { + req, + path: path.into(), + }), + ) + } + + /// Read a whole file. The reply arrives as a stream of chunks that + /// [`FileClient::accept`] reassembles; registers the in-flight read here. + pub fn read(&mut self, path: impl Into) -> (u64, FileFrame) { + let req = self.alloc(); + self.reads.insert(req, ReadAccum::default()); + ( + req, + self.frame(FileEvent::Read { + req, + path: path.into(), + }), + ) + } + + /// Write one piece of a file. The first piece should set `append = false` + /// (create/truncate); later pieces `true`. The piece with `eof = true` + /// commits, and the host answers [`FileEvent::Ok`]. + pub fn write( + &mut self, + req: u64, + path: impl Into, + data: Vec, + append: bool, + eof: bool, + ) -> FileFrame { + self.frame(FileEvent::Write { + req, + path: path.into(), + data, + append, + eof, + }) + } + + /// Allocate a request id to thread through a multi-piece [`FileClient::write`]. + pub fn begin_write(&mut self) -> u64 { + self.alloc() + } + + pub fn mkdir(&mut self, path: impl Into) -> (u64, FileFrame) { + let req = self.alloc(); + ( + req, + self.frame(FileEvent::Mkdir { + req, + path: path.into(), + }), + ) + } + + pub fn rename(&mut self, from: impl Into, to: impl Into) -> (u64, FileFrame) { + let req = self.alloc(); + ( + req, + self.frame(FileEvent::Rename { + req, + from: from.into(), + to: to.into(), + }), + ) + } + + pub fn delete(&mut self, path: impl Into) -> (u64, FileFrame) { + let req = self.alloc(); + ( + req, + self.frame(FileEvent::Delete { + req, + path: path.into(), + }), + ) + } + + /// Interpret one inbound host frame. For a `Read`, accumulates the chunk + /// and only yields [`FileReply::ReadDone`] on the final piece (with + /// [`FileReply::ReadProgress`] for the others). Returns `None` for a frame + /// that isn't this route's, isn't a host *reply*, or is a chunk for a read + /// we never started (a stray after teardown). + pub fn accept(&mut self, frame: &FileFrame) -> Option { + if frame.route != self.route { + return None; + } + match &frame.event { + FileEvent::Entries { + req, + path, + home, + entries, + } => Some(FileReply::Listing { + req: *req, + path: path.clone(), + home: home.clone(), + entries: entries.clone(), + }), + FileEvent::Chunk { + req, + data, + total, + eof, + } => { + let acc = self.reads.get_mut(req)?; + acc.data.extend_from_slice(data); + acc.total = *total; + if *eof { + let acc = self.reads.remove(req).unwrap_or_default(); + Some(FileReply::ReadDone { + req: *req, + data: acc.data, + }) + } else { + Some(FileReply::ReadProgress { + req: *req, + received: acc.data.len() as u64, + total: acc.total, + }) + } + } + FileEvent::Ok { req } => Some(FileReply::Ok { req: *req }), + FileEvent::Err { req, reason } => Some(FileReply::Err { + req: *req, + reason: reason.clone(), + }), + // Requests (we send these) and unknown future events aren't replies. + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use allmystuff_session::MediaPayload; + + fn val(f: &FileFrame) -> serde_json::Value { + serde_json::to_value(f).unwrap() + } + + /// Serialize a host-built frame and decode it the way the media channel + /// does, so the reply path is exercised end-to-end. + fn host_frame(route: &str, event: FileEvent) -> FileFrame { + let mut host = FileClient::new(route); + let f = host.frame(event); + match MediaPayload::decode(serde_json::to_value(&f).unwrap()) { + Some(MediaPayload::File(decoded)) => decoded, + other => panic!("expected a file frame, got {other:?}"), + } + } + + #[test] + fn requests_get_distinct_ids_starting_at_one() { + let mut fc = FileClient::new("route:desk:files→phone:files-view:1"); + let (r1, list) = fc.list(""); + let (r2, _read) = fc.read("/etc/hosts"); + assert_eq!(r1, 1); + assert_eq!(r2, 2); + let j = val(&list); + assert_eq!(j["t"], "file"); + assert_eq!(j["kind"], "list"); + assert_eq!(j["req"], 1); + assert_eq!(j["path"], ""); + } + + #[test] + fn listing_reply_is_surfaced() { + let mut fc = FileClient::new("route:r"); + let (req, _) = fc.list("/home/me"); + let reply = host_frame( + "route:r", + FileEvent::Entries { + req, + path: "/home/me".into(), + home: "/home/me".into(), + entries: vec![FileEntry { + name: "notes.txt".into(), + dir: false, + size: 12, + modified: None, + symlink: false, + }], + }, + ); + match fc.accept(&reply) { + Some(FileReply::Listing { + path, + home, + entries, + .. + }) => { + assert_eq!(path, "/home/me"); + assert_eq!(home, "/home/me"); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].name, "notes.txt"); + } + other => panic!("expected a listing, got {other:?}"), + } + } + + #[test] + fn a_read_reassembles_across_chunks() { + let mut fc = FileClient::new("route:r"); + let (req, _read) = fc.read("/tmp/f"); + + let c1 = host_frame( + "route:r", + FileEvent::Chunk { + req, + data: b"hello ".to_vec(), + total: 11, + eof: false, + }, + ); + let c2 = host_frame( + "route:r", + FileEvent::Chunk { + req, + data: b"world".to_vec(), + total: 11, + eof: true, + }, + ); + + match fc.accept(&c1) { + Some(FileReply::ReadProgress { + received, total, .. + }) => { + assert_eq!((received, total), (6, 11)); + } + other => panic!("expected progress, got {other:?}"), + } + match fc.accept(&c2) { + Some(FileReply::ReadDone { data, .. }) => assert_eq!(data, b"hello world"), + other => panic!("expected the finished read, got {other:?}"), + } + // The read is finished — a stray late chunk for it is ignored. + let stray = host_frame( + "route:r", + FileEvent::Chunk { + req, + data: b"!".to_vec(), + total: 11, + eof: true, + }, + ); + assert!(fc.accept(&stray).is_none()); + } + + #[test] + fn ok_and_err_replies_carry_their_req() { + let mut fc = FileClient::new("route:r"); + let (req, _) = fc.mkdir("/tmp/new"); + let ok = host_frame("route:r", FileEvent::Ok { req }); + assert_eq!(fc.accept(&ok), Some(FileReply::Ok { req })); + + let (req2, _) = fc.delete("/tmp/locked"); + let err = host_frame( + "route:r", + FileEvent::Err { + req: req2, + reason: "permission denied".into(), + }, + ); + assert_eq!( + fc.accept(&err), + Some(FileReply::Err { + req: req2, + reason: "permission denied".into() + }) + ); + } + + #[test] + fn frames_for_another_route_are_ignored() { + let mut fc = FileClient::new("route:mine"); + let other = host_frame("route:other", FileEvent::Ok { req: 1 }); + assert!(fc.accept(&other).is_none()); + } + + #[test] + fn a_multi_piece_write_threads_one_req() { + let mut fc = FileClient::new("route:r"); + let req = fc.begin_write(); + let first = val(&fc.write(req, "/tmp/up", b"part1".to_vec(), false, false)); + let last = val(&fc.write(req, "/tmp/up", b"part2".to_vec(), true, true)); + assert_eq!(first["kind"], "write"); + assert_eq!(first["req"], req); + assert_eq!(first["append"], false); + assert_eq!(last["append"], true); + assert_eq!(last["eof"], true); + } +} diff --git a/crates/allmystuff-mobile-core/src/media/input.rs b/crates/allmystuff-mobile-core/src/media/input.rs new file mode 100644 index 00000000..31dad18a --- /dev/null +++ b/crates/allmystuff-mobile-core/src/media/input.rs @@ -0,0 +1,195 @@ +//! The input *encode* path: turn what a finger (or the on-screen keyboard) +//! does into the normalized [`InputAction`]s a remote's `control` sink +//! injects. +//! +//! The wire contract is deliberately resolution-free: pointer coordinates are +//! normalized `0..1` over the *source screen* of the paired display route, so +//! the phone never needs the remote's pixel size, and keys travel as DOM +//! `KeyboardEvent.key` / `.code` strings, so layouts resolve on the remote. +//! That makes the phone's job pure arithmetic, which is all this module is. +//! +//! Each [`InputEvent`] carries a monotonic `seq` per route so the remote can +//! spot drops/reorders; [`InputEncoder`] owns that counter. + +use allmystuff_session::{InputAction, InputEvent}; + +/// The DOM mouse-button convention the wire uses: 0 left, 1 middle, 2 right. +pub const BUTTON_LEFT: u8 = 0; +pub const BUTTON_MIDDLE: u8 = 1; +pub const BUTTON_RIGHT: u8 = 2; + +/// Encodes a phone's pointer/keyboard intent for one input route, minting the +/// per-route sequence numbers as it goes. +#[derive(Debug, Clone)] +pub struct InputEncoder { + route: String, + seq: u64, + /// Which of the remote's monitors coordinates are normalized over — set + /// to the `screen:` the console is showing (`None` = primary). + screen: Option, +} + +impl InputEncoder { + /// A new encoder for `route`, targeting the remote's primary screen. + pub fn new(route: impl Into) -> Self { + InputEncoder { + route: route.into(), + seq: 0, + screen: None, + } + } + + /// Aim subsequent moves at a specific remote monitor (`:screen:` + /// → the bare ``), so control follows the screen the viewer is showing. + pub fn on_screen(mut self, screen: Option) -> Self { + self.screen = screen; + self + } + + fn next(&mut self, action: InputAction) -> InputEvent { + let seq = self.seq; + self.seq += 1; + InputEvent::new(self.route.clone(), seq, action) + } + + /// Move the pointer to a normalized `(x, y)` (each clamped to `0..=1`). + pub fn move_to(&mut self, x: f64, y: f64) -> InputEvent { + self.next(InputAction::MouseMove { + x: clamp01(x), + y: clamp01(y), + screen: self.screen, + }) + } + + /// Press or release a mouse button. + pub fn button(&mut self, button: u8, down: bool) -> InputEvent { + self.next(InputAction::MouseButton { button, down }) + } + + /// Scroll, in wheel lines (positive = down / right). + pub fn wheel(&mut self, dx: f64, dy: f64) -> InputEvent { + self.next(InputAction::Wheel { dx, dy }) + } + + /// Press or release a key. `key` is the DOM `KeyboardEvent.key` value + /// (`"a"`, `"Enter"`, `"ArrowLeft"`); `code` is the physical + /// `KeyboardEvent.code` (`"KeyC"`, `"Digit1"`) — pass it whenever you have + /// it, since chords (Ctrl+C, Shift+1) need the physical key to land and + /// release correctly regardless of the held modifiers. + pub fn key(&mut self, key: impl Into, code: Option, down: bool) -> InputEvent { + self.next(InputAction::Key { + key: key.into(), + code, + down, + }) + } + + /// A complete left-click at a normalized point: move, press, release. + /// The natural mapping of a tap. Returns the three events in order. + pub fn tap(&mut self, x: f64, y: f64) -> Vec { + vec![ + self.move_to(x, y), + self.button(BUTTON_LEFT, true), + self.button(BUTTON_LEFT, false), + ] + } + + /// A complete key *tap*: down then up. The natural mapping of a + /// soft-keyboard press. + pub fn type_key( + &mut self, + key: impl Into + Clone, + code: Option, + ) -> Vec { + vec![ + self.key(key.clone(), code.clone(), true), + self.key(key, code, false), + ] + } +} + +/// Clamp to the unit interval — a stray gesture off the edge of the stage +/// can't drive the remote pointer past its own screen. +fn clamp01(v: f64) -> f64 { + v.clamp(0.0, 1.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn val(e: &InputEvent) -> serde_json::Value { + serde_json::to_value(e).unwrap() + } + + #[test] + fn moves_are_normalized_and_clamped() { + let mut enc = InputEncoder::new("route:desk:screen→phone:display-in"); + let e = enc.move_to(1.4, -0.2); + let j = val(&e); + assert_eq!(j["t"], "input"); + assert_eq!(j["kind"], "mouse_move"); + assert_eq!(j["x"], 1.0); // clamped down from 1.4 + assert_eq!(j["y"], 0.0); // clamped up from -0.2 + assert_eq!(j["seq"], 0); + // Primary screen → the screen key is omitted entirely. + assert!(j.get("screen").is_none() || j["screen"].is_null()); + } + + #[test] + fn on_screen_threads_the_monitor_id() { + let mut enc = InputEncoder::new("r").on_screen(Some(2)); + let j = val(&enc.move_to(0.5, 0.5)); + assert_eq!(j["screen"], 2); + } + + #[test] + fn seq_is_monotonic_per_encoder() { + let mut enc = InputEncoder::new("r"); + assert_eq!(val(&enc.move_to(0.0, 0.0))["seq"], 0); + assert_eq!(val(&enc.button(BUTTON_LEFT, true))["seq"], 1); + assert_eq!(val(&enc.button(BUTTON_LEFT, false))["seq"], 2); + } + + #[test] + fn tap_expands_to_move_press_release_with_running_seqs() { + let mut enc = InputEncoder::new("r"); + let events = enc.tap(0.25, 0.75); + assert_eq!(events.len(), 3); + let kinds: Vec<_> = events + .iter() + .map(|e| val(e)["kind"].as_str().unwrap().to_string()) + .collect(); + assert_eq!(kinds, ["mouse_move", "mouse_button", "mouse_button"]); + assert_eq!(val(&events[1])["down"], true); + assert_eq!(val(&events[2])["down"], false); + // seqs run 0,1,2 across the tap. + let seqs: Vec<_> = events + .iter() + .map(|e| val(e)["seq"].as_u64().unwrap()) + .collect(); + assert_eq!(seqs, [0, 1, 2]); + } + + #[test] + fn key_carries_code_for_chords() { + let mut enc = InputEncoder::new("r"); + let j = val(&enc.key("c", Some("KeyC".into()), true)); + assert_eq!(j["kind"], "key"); + assert_eq!(j["key"], "c"); + assert_eq!(j["code"], "KeyC"); + assert_eq!(j["down"], true); + + // A key with no physical code omits it (older-receiver compatible). + let j2 = val(&enc.key("Enter", None, true)); + assert!(j2.get("code").is_none() || j2["code"].is_null()); + } + + #[test] + fn type_key_is_down_then_up() { + let mut enc = InputEncoder::new("r"); + let evs = enc.type_key("a", Some("KeyA".into())); + assert_eq!(val(&evs[0])["down"], true); + assert_eq!(val(&evs[1])["down"], false); + } +} diff --git a/crates/allmystuff-mobile-core/src/media/mod.rs b/crates/allmystuff-mobile-core/src/media/mod.rs new file mode 100644 index 00000000..db744a21 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/media/mod.rs @@ -0,0 +1,32 @@ +//! The client side of each media plane, as pure logic over the +//! [`allmystuff_session`] frame types. +//! +//! A phone is a *consumer and controller*, so each module here is one half of +//! a plane: +//! +//! * [`video`] — **decode** path: reassemble chunked MJPEG off the media +//! channel ([`VideoSink`]) and define the [`VideoDecoder`] seam the platform +//! H.264 decoder (VideoToolbox / MediaCodec) fills in for the RTP lane. +//! * [`input`] — **encode** path: turn touches, drags, wheels and key presses +//! into normalized [`InputEvent`](allmystuff_session::InputEvent)s. +//! * [`term`] — **both** directions of a terminal: keystrokes + resizes up, +//! PTY bytes down, for an xterm.js view. +//! * [`files`] — a request/reply [`FileClient`] that mints request ids, +//! builds [`FileEvent`](allmystuff_session::FileEvent)s, and reassembles the +//! `Chunk` stream a `Read` answers with. +//! +//! None of this touches a codec or a socket: the platform layer owns the +//! H.264/Opus/JPEG decoders and the radio. This is the part that has to be +//! right on the byte, so it lives where `cargo test` can prove it. + +pub mod files; +pub mod input; +pub mod term; +pub mod video; + +pub use files::{FileClient, FileReply}; +pub use input::InputEncoder; +pub use term::TermPlane; +pub use video::{ + CompressedFrame, H264Au, JpegFrame, RgbaFrame, VideoDecoder, VideoSink, VideoUpdate, +}; diff --git a/crates/allmystuff-mobile-core/src/media/term.rs b/crates/allmystuff-mobile-core/src/media/term.rs new file mode 100644 index 00000000..e8f43f75 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/media/term.rs @@ -0,0 +1,132 @@ +//! A terminal route, both directions, for an xterm.js view on the phone. +//! +//! Bytes are opaque on the wire — the emulator and the remote PTY speak VT +//! between themselves; the phone just carries keystrokes up +//! ([`TermEvent::Data`]) and resizes up ([`TermEvent::Resize`]), and renders +//! the PTY bytes that come down. [`TermPlane`] owns the per-route `seq` +//! counter for what it sends and demuxes what arrives. + +use allmystuff_session::{MediaPayload, TermEvent, TermFrame}; + +/// One end of a terminal route. Send keystrokes + resizes; feed inbound media +/// payloads to [`TermPlane::accept`] to pull out the PTY output for this +/// route. +#[derive(Debug, Clone)] +pub struct TermPlane { + route: String, + seq: u64, +} + +impl TermPlane { + pub fn new(route: impl Into) -> Self { + TermPlane { + route: route.into(), + seq: 0, + } + } + + pub fn route(&self) -> &str { + &self.route + } + + fn next(&mut self, event: TermEvent) -> TermFrame { + let seq = self.seq; + self.seq += 1; + TermFrame::new(self.route.clone(), seq, event) + } + + /// Send raw keystroke bytes to the remote shell (what xterm.js produces + /// from a keypress, paste, or the on-screen key bar). + pub fn send(&mut self, bytes: impl Into>) -> TermFrame { + self.next(TermEvent::Data { + bytes: bytes.into(), + }) + } + + /// Tell the host the emulator's new size so it resizes the PTY and the + /// shell reflows. + pub fn resize(&mut self, cols: u16, rows: u16) -> TermFrame { + self.next(TermEvent::Resize { cols, rows }) + } + + /// Pull this route's terminal event out of an inbound media payload, or + /// `None` if it's not a terminal frame for *this* route. Returns the + /// host's [`TermEvent::Data`] (bytes to write to the emulator) or + /// [`TermEvent::Exit`] (the shell ended). + pub fn accept(&self, payload: &MediaPayload) -> Option { + match payload { + MediaPayload::Terminal(frame) if frame.route == self.route => Some(frame.event.clone()), + _ => None, + } + } + + /// Convenience over [`TermPlane::accept`]: just the PTY bytes to paint, + /// dropping resize/exit/unknown events. + pub fn accept_output(&self, payload: &MediaPayload) -> Option> { + match self.accept(payload) { + Some(TermEvent::Data { bytes }) => Some(bytes), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn val(f: &TermFrame) -> serde_json::Value { + serde_json::to_value(f).unwrap() + } + + #[test] + fn keystrokes_carry_base64_data_and_increment_seq() { + let mut t = TermPlane::new("route:desk:terminal→phone:term-view:1"); + let a = val(&t.send(b"ls\n".to_vec())); + assert_eq!(a["t"], "term"); + assert_eq!(a["kind"], "data"); + assert_eq!(a["seq"], 0); + // bytes travel as a base64 string, never a number array. + assert!(a["bytes"].is_string()); + + let b = val(&t.resize(120, 40)); + assert_eq!(b["kind"], "resize"); + assert_eq!(b["cols"], 120); + assert_eq!(b["rows"], 40); + assert_eq!(b["seq"], 1); + } + + #[test] + fn round_trips_a_data_frame_back_to_its_bytes() { + // Build the frame the host would send, by serializing one we made and + // decoding it the way the media channel does. + let mut host = TermPlane::new("route:r"); + let on_wire = serde_json::to_value(host.send(b"hi".to_vec())).unwrap(); + let payload = MediaPayload::decode(on_wire).expect("decodes as a term frame"); + + let viewer = TermPlane::new("route:r"); + assert_eq!(viewer.accept_output(&payload), Some(b"hi".to_vec())); + } + + #[test] + fn output_for_a_different_route_is_ignored() { + let mut host = TermPlane::new("route:other"); + let payload = + MediaPayload::decode(serde_json::to_value(host.send(b"x".to_vec())).unwrap()).unwrap(); + let viewer = TermPlane::new("route:mine"); + assert!(viewer.accept(&payload).is_none()); + } + + #[test] + fn exit_is_surfaced_but_not_as_output() { + let mut host = TermPlane::new("route:r"); + // Serialize an Exit frame and decode it. + let exit = host.next(TermEvent::Exit { code: Some(0) }); + let payload = MediaPayload::decode(serde_json::to_value(exit).unwrap()).unwrap(); + let viewer = TermPlane::new("route:r"); + assert!(matches!( + viewer.accept(&payload), + Some(TermEvent::Exit { code: Some(0) }) + )); + assert!(viewer.accept_output(&payload).is_none()); + } +} diff --git a/crates/allmystuff-mobile-core/src/media/video.rs b/crates/allmystuff-mobile-core/src/media/video.rs new file mode 100644 index 00000000..7cb03703 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/media/video.rs @@ -0,0 +1,286 @@ +//! The video decode path on a phone. +//! +//! Two transports can carry a screen/camera stream, and the phone consumes +//! both: +//! +//! * **MJPEG over the media channel** — standalone baseline JPEGs, possibly +//! chunked across several ~64 KiB messages sharing a `seq`. [`VideoSink`] +//! demuxes these off the channel and reassembles whole frames with +//! [`VideoAssembler`], handing the platform a ready-to-decode [`JpegFrame`]. +//! +//! * **H.264 over an RTP track lane** — Annex-B access units delivered by the +//! mesh's `video_inbound` lane. The phone feeds these straight to a hardware +//! decoder (VideoToolbox on iOS, MediaCodec on Android) as an [`H264Au`]. +//! +//! Both converge on one seam — [`VideoDecoder`] — which turns a +//! [`CompressedFrame`] into an [`RgbaFrame`] the UI paints. This crate defines +//! the seam and the MJPEG reassembly (pure, testable); the actual JPEG/H.264 +//! decoding is platform code that implements [`VideoDecoder`]. We decode +//! *on the phone* from the compressed bytes that already crossed the mesh — +//! never pull raw RGBA over the network. + +use allmystuff_session::{MediaPayload, VideoAssembler, VideoFrame, VideoStatusFrame}; + +/// A frame of pixels ready to paint: tightly-packed RGBA8888, row-major, +/// `width * height * 4` bytes. `source_*` is the capture's full resolution, +/// which normalized input coordinates map back onto (see +/// [`crate::media::InputEncoder`]). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RgbaFrame { + pub route: String, + pub width: u32, + pub height: u32, + pub source_width: u32, + pub source_height: u32, + pub rgba: Vec, +} + +/// One complete JPEG, reassembled off the media channel — ready for the +/// platform's JPEG decoder. Distilled from a [`VideoFrame`] once all its +/// chunks have arrived. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JpegFrame { + pub route: String, + pub seq: u64, + pub width: u32, + pub height: u32, + pub source_width: u32, + pub source_height: u32, + pub jpeg: Vec, +} + +impl From for JpegFrame { + fn from(f: VideoFrame) -> Self { + JpegFrame { + route: f.route, + seq: f.seq, + width: f.width, + height: f.height, + source_width: f.source_width, + source_height: f.source_height, + jpeg: f.jpeg, + } + } +} + +/// One H.264 access unit off the RTP lane, ready for a hardware decoder. +/// `key` marks an IDR (a clean decode entry point); `pts_micros` is the +/// presentation timestamp in microseconds (the lane's `rtp_timestamp` +/// rescaled from the 90 kHz clock: `rtp_ts * 1000 / 90`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct H264Au { + pub route: String, + /// Annex-B byte stream (start-code-delimited NAL units). + pub data: Vec, + pub key: bool, + pub pts_micros: i64, +} + +/// What a [`VideoDecoder`] is handed — whichever transport the stream took. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CompressedFrame { + /// A reassembled MJPEG frame from the media channel. + Jpeg(JpegFrame), + /// An H.264 access unit from the RTP lane. + H264(H264Au), +} + +impl CompressedFrame { + pub fn route(&self) -> &str { + match self { + CompressedFrame::Jpeg(f) => &f.route, + CompressedFrame::H264(f) => &f.route, + } + } +} + +/// The platform decode seam. iOS implements this over VideoToolbox, Android +/// over MediaCodec; tests can implement a trivial one. A decoder is per-route +/// (it holds the H.264 reference state), so the app keeps one per live stream. +pub trait VideoDecoder { + /// Decode one compressed frame to RGBA, or `None` if this frame only + /// advanced decoder state and produced no displayable picture yet (e.g. a + /// non-key H.264 AU before the first IDR). + fn decode(&mut self, frame: &CompressedFrame) -> Option; + + /// The decoder lost its place (a decode error, a rebuilt pipeline). The + /// app should follow this with a [`RouteControl::Refresh`] to the streamer + /// so the next access unit is an IDR rather than waiting out the periodic + /// interval. + /// + /// [`RouteControl::Refresh`]: allmystuff_protocol::RouteControl::Refresh + fn refresh(&mut self) {} +} + +/// What demuxing one media-channel payload through a [`VideoSink`] produced. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VideoUpdate { + /// A complete MJPEG frame is ready to decode + paint. + Frame(JpegFrame), + /// The host reported why the stream is (not) producing pixels — render + /// this instead of a silent black screen. + Status(VideoStatusFrame), +} + +/// Demuxes a display/camera route's frames off the shared media channel and +/// reassembles chunked MJPEG. One sink per phone is enough — it keys partial +/// frames by route internally (via [`VideoAssembler`]). +#[derive(Debug, Default)] +pub struct VideoSink { + assembler: VideoAssembler, +} + +impl VideoSink { + pub fn new() -> Self { + Self::default() + } + + /// Feed one decoded media-channel payload. Returns a [`VideoUpdate`] when + /// a whole frame (or a status change) is ready; `None` while a chunked + /// frame is still arriving, or for a payload that isn't this plane's. + pub fn accept(&mut self, payload: MediaPayload) -> Option { + match payload { + MediaPayload::Video(frame) => self + .assembler + .push(frame) + .map(|full| VideoUpdate::Frame(full.into())), + MediaPayload::VideoStatus(status) => Some(VideoUpdate::Status(status)), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::Engine as _; + + /// Build a `VideoFrame` the way the wire does — through JSON, with the + /// jpeg bytes base64'd — since the `MediaTagVideo` tag type is private to + /// allmystuff-session. + fn chunk(route: &str, seq: u64, idx: u16, of: u16, jpeg: Vec) -> VideoFrame { + let b64 = base64::engine::general_purpose::STANDARD.encode(&jpeg); + serde_json::from_value(serde_json::json!({ + "t": "video", + "route": route, + "seq": seq, + "width": 1920, + "height": 1080, + "source_width": 3840, + "source_height": 2160, + "chunk": idx, + "chunks": of, + "jpeg": b64, + })) + .expect("valid VideoFrame json") + } + + #[test] + fn whole_frame_passes_straight_through() { + let mut sink = VideoSink::new(); + let out = sink.accept(MediaPayload::Video(chunk( + "r", + 1, + 0, + 1, + vec![0xFF, 0xD8, 0xFF], + ))); + match out { + Some(VideoUpdate::Frame(f)) => { + assert_eq!(f.seq, 1); + assert_eq!(f.jpeg, vec![0xFF, 0xD8, 0xFF]); + assert_eq!((f.source_width, f.source_height), (3840, 2160)); + } + other => panic!("expected a frame, got {other:?}"), + } + } + + #[test] + fn chunked_frame_only_emits_once_every_piece_arrives() { + let mut sink = VideoSink::new(); + // First of two chunks → nothing yet. + assert!(sink + .accept(MediaPayload::Video(chunk("r", 5, 0, 2, vec![1, 2, 3]))) + .is_none()); + // Second chunk completes it → the reassembled, concatenated frame. + let out = sink.accept(MediaPayload::Video(chunk("r", 5, 1, 2, vec![4, 5]))); + match out { + Some(VideoUpdate::Frame(f)) => assert_eq!(f.jpeg, vec![1, 2, 3, 4, 5]), + other => panic!("expected the reassembled frame, got {other:?}"), + } + } + + #[test] + fn non_video_payloads_are_ignored_by_the_video_sink() { + let mut sink = VideoSink::new(); + let audio = MediaPayload::Audio(allmystuff_session::AudioFrame::new( + "r", + 0, + 48000, + 1, + vec![0], + )); + assert!(sink.accept(audio).is_none()); + } + + // A stand-in decoder: "decodes" a JPEG by echoing its bytes as fake RGBA, + // and only produces a picture from an H.264 key frame — enough to exercise + // the seam. + struct FakeDecoder; + impl VideoDecoder for FakeDecoder { + fn decode(&mut self, frame: &CompressedFrame) -> Option { + match frame { + CompressedFrame::Jpeg(f) => Some(RgbaFrame { + route: f.route.clone(), + width: f.width, + height: f.height, + source_width: f.source_width, + source_height: f.source_height, + rgba: f.jpeg.clone(), + }), + CompressedFrame::H264(au) if au.key => Some(RgbaFrame { + route: au.route.clone(), + width: 0, + height: 0, + source_width: 0, + source_height: 0, + rgba: au.data.clone(), + }), + CompressedFrame::H264(_) => None, + } + } + } + + #[test] + fn decoder_seam_handles_both_transports() { + let mut dec = FakeDecoder; + let jpeg = CompressedFrame::Jpeg(JpegFrame { + route: "r".into(), + seq: 0, + width: 2, + height: 1, + source_width: 2, + source_height: 1, + jpeg: vec![9, 9], + }); + assert_eq!(dec.decode(&jpeg).unwrap().rgba, vec![9, 9]); + + // A delta frame before any keyframe yields no picture... + let delta = CompressedFrame::H264(H264Au { + route: "r".into(), + data: vec![1], + key: false, + pts_micros: 0, + }); + assert!(dec.decode(&delta).is_none()); + // ...a keyframe does. + let key = CompressedFrame::H264(H264Au { + route: "r".into(), + data: vec![2], + key: true, + pts_micros: 1_000, + }); + assert_eq!(dec.decode(&key).unwrap().rgba, vec![2]); + assert_eq!(key.route(), "r"); + } +} diff --git a/crates/allmystuff-mobile-core/src/node.rs b/crates/allmystuff-mobile-core/src/node.rs new file mode 100644 index 00000000..c8e1aab0 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/node.rs @@ -0,0 +1,145 @@ +//! Assemble the phone's [`NodeProfile`] — what it tells the mesh about +//! itself on the presence channel. +//! +//! A phone has no hardware scan behind it (there is no `Inventory` here), so +//! the profile is built straight from a small config plus the synthetic +//! capability set from [`crate::caps`]. The shape is identical to a desktop's +//! profile, so a peer renders a phone on the same graph with no special case. + +use allmystuff_graph::NodeId; +use allmystuff_protocol::{InventorySummary, NodeProfile, PROTOCOL_VERSION}; + +use crate::caps::{mobile_capabilities, mobile_features, MobileScope}; + +/// Everything platform code knows about the phone, distilled to what the +/// presence advert needs. `boot` and `version` are supplied by the caller +/// (this crate has no clock and no build metadata of its own). +#[derive(Debug, Clone)] +pub struct MobileNodeConfig { + /// Display name — "Chris's iPhone". Falls back to `model` if empty. + pub label: String, + /// The host OS string for the node card — "iOS 18.4", "Android 15". + pub os: String, + /// Marketing model name shown where a desktop shows its CPU — "iPhone 15 + /// Pro", "Pixel 9". + pub model: String, + /// Total RAM in bytes, if the platform offers it (`0` = unknown). + pub ram_bytes: u64, + /// How much of itself this phone exposes. + pub scope: MobileScope, +} + +impl MobileNodeConfig { + /// A minimal viewer/controller config — just the two strings a phone + /// always knows. + pub fn new(label: impl Into, os: impl Into) -> Self { + MobileNodeConfig { + label: label.into(), + os: os.into(), + model: String::new(), + ram_bytes: 0, + scope: MobileScope::ViewerController, + } + } +} + +/// Build the phone's presence profile. +/// +/// * `node` is the phone's mesh id (its ed25519-derived device id). +/// * `boot` is a per-run random id — a peer that sees a new one knows the +/// phone restarted (the same event-driven gossip the desktop uses). +/// * `version` is the app's `CARGO_PKG_VERSION`. +pub fn mobile_profile( + node: &NodeId, + cfg: &MobileNodeConfig, + boot: u64, + version: impl Into, +) -> NodeProfile { + let capabilities = mobile_capabilities(node, cfg.scope); + // The "N things" headline counts what the phone can *offer* (its sources), + // so a viewer phone reads as a lean node, a host phone as a few more. + let device_count = capabilities.iter().filter(|c| c.flow.can_source()).count() as u32; + let label = if cfg.label.trim().is_empty() { + cfg.model.clone() + } else { + cfg.label.clone() + }; + + NodeProfile { + protocol: PROTOCOL_VERSION, + node: node.clone(), + label, + // A phone has no separate hostname; the label is the whole story. + hostname: String::new(), + summary: InventorySummary { + os: cfg.os.clone(), + cpu: cfg.model.clone(), + ram_bytes: cfg.ram_bytes, + device_count, + }, + capabilities, + // Ownership is established by pairing into a fleet, not asserted here. + owner: None, + // A phone is never put up for adoption — it claims, it isn't claimed. + claimable: false, + boot, + features: mobile_features(cfg.scope), + // A phone exposes no reverse-proxied sites. + sites: Vec::new(), + version: version.into(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profile_carries_the_phones_caps_and_features() { + let me = NodeId::from("phone-xyz"); + let cfg = MobileNodeConfig { + label: "Chris's iPhone".into(), + os: "iOS 18.4".into(), + model: "iPhone 15 Pro".into(), + ram_bytes: 8 << 30, + scope: MobileScope::ViewerController, + }; + let p = mobile_profile(&me, &cfg, 42, "0.2.4"); + + assert_eq!(p.protocol, PROTOCOL_VERSION); + assert_eq!(p.node.as_str(), "phone-xyz"); + assert_eq!(p.label, "Chris's iPhone"); + assert_eq!(p.summary.os, "iOS 18.4"); + assert_eq!(p.summary.cpu, "iPhone 15 Pro"); + assert!(!p.claimable); + assert!(p.owner.is_none()); + assert_eq!(p.version, "0.2.4"); + assert!(p.features.iter().any(|f| f == "terminal")); + assert!(p.capabilities.iter().any(|c| c.origin == "viewer")); + // A viewer phone offers two source-capable endpoints: touch control + // (input source) and the clipboard (duplex). + assert_eq!(p.summary.device_count, 2); + + // The whole profile round-trips through JSON like any other peer's. + let json = serde_json::to_string(&p).unwrap(); + let back: NodeProfile = serde_json::from_str(&json).unwrap(); + assert_eq!(p, back); + } + + #[test] + fn empty_label_falls_back_to_model() { + let me = NodeId::from("phone-1"); + let cfg = MobileNodeConfig { + label: " ".into(), + os: "Android 15".into(), + model: "Pixel 9".into(), + ram_bytes: 0, + scope: MobileScope::ViewerControllerHost, + }; + let p = mobile_profile(&me, &cfg, 1, "0.2.4"); + assert_eq!(p.label, "Pixel 9"); + // Host scope adds camera + mic + screen sources on top of the viewer's + // two (touch control + clipboard) → 5 offerable endpoints. + assert_eq!(p.summary.device_count, 5); + } +} diff --git a/crates/allmystuff-mobile-core/src/transport.rs b/crates/allmystuff-mobile-core/src/transport.rs new file mode 100644 index 00000000..0f63e7d3 --- /dev/null +++ b/crates/allmystuff-mobile-core/src/transport.rs @@ -0,0 +1,251 @@ +//! The seam between this pure logic and the embedded mesh. +//! +//! On device, the app links `myownmesh-core` (cross-compiled, via a UniFFI +//! layer) and implements [`MeshClient`] over a [`JoinedNetwork`]: presence, +//! the control channel, and the media channel all map onto the engine's typed +//! `Channel` API. In tests, an in-memory fake implements the same trait, so +//! every offer/observe path in this crate runs without a radio. +//! +//! Inbound traffic arrives as raw `(channel, from, payload)` triples off the +//! mesh; [`classify`] turns one into a typed [`Inbound`] using the channel +//! constants and the same lenient decoders the desktop uses — an +//! unrecognised payload yields `None` (or a `*::Unknown`), never an error, so +//! a newer peer can't knock the phone off the channel. +//! +//! [`JoinedNetwork`]: https://docs.rs/myownmesh-core + +use allmystuff_protocol::{ + ControlMessage, NodeProfile, OwnedRoster, RoomMessage, CHANNEL_CONTROL, CHANNEL_MEDIA, + CHANNEL_OWNED, CHANNEL_PRESENCE, CHANNEL_ROOMS, +}; +use allmystuff_session::MediaPayload; + +/// What couldn't be done over the mesh. +#[derive(Debug, thiserror::Error)] +pub enum MeshError { + /// No live network to send on (not yet joined, or torn down). + #[error("not connected to a network")] + NotConnected, + /// The peer is unknown / unreachable on this network. + #[error("no such peer: {0}")] + NoSuchPeer(String), + /// The engine refused or failed the send; carries its message. + #[error("mesh send failed: {0}")] + Send(String), + /// A payload wouldn't serialize. + #[error(transparent)] + Encode(#[from] serde_json::Error), +} + +pub type MeshResult = Result; + +/// One typed thing the phone took off the mesh. +#[derive(Debug, Clone, PartialEq)] +pub enum Inbound { + /// A peer's presence advert (its [`NodeProfile`] carries its node id). + Presence(NodeProfile), + /// A control message from `from` — route offers/accepts, share/ownership + /// negotiation, app control. + Control { from: String, msg: ControlMessage }, + /// A media-channel frame from `from` — video/audio/input/terminal/files/ + /// clipboard/site. Feed it to the matching plane in [`crate::media`]. + Media { from: String, payload: MediaPayload }, + /// A fleet roster update (the owned-devices channel). + Owned(OwnedRoster), + /// A rooms-channel message from `from`. + Room { from: String, msg: RoomMessage }, +} + +/// Turn one raw inbound `(channel, from, payload)` into a typed [`Inbound`]. +/// +/// `None` when the channel isn't one AllMyStuff speaks, or the payload doesn't +/// decode — both are dropped silently, the forward-compatible default. A +/// control/room message with a tag this build doesn't know decodes to its +/// enum's `Unknown` variant rather than `None`, so the envelope still arrives +/// (and is ignored downstream). +pub fn classify(channel: &str, from: &str, payload: serde_json::Value) -> Option { + match channel { + CHANNEL_PRESENCE => serde_json::from_value(payload).ok().map(Inbound::Presence), + CHANNEL_CONTROL => serde_json::from_value(payload) + .ok() + .map(|msg| Inbound::Control { + from: from.to_string(), + msg, + }), + CHANNEL_MEDIA => MediaPayload::decode(payload).map(|payload| Inbound::Media { + from: from.to_string(), + payload, + }), + CHANNEL_OWNED => serde_json::from_value(payload).ok().map(Inbound::Owned), + CHANNEL_ROOMS => serde_json::from_value(payload) + .ok() + .map(|msg| Inbound::Room { + from: from.to_string(), + msg, + }), + _ => None, + } +} + +/// The outbound mesh surface a phone needs — the small slice of +/// `myownmesh-core`'s `JoinedNetwork` this crate drives. The platform layer +/// implements it over the embedded engine; tests implement an in-memory fake. +/// +/// All sends name a `peer` (its mesh device id), because AllMyStuff publishes +/// to specific peers, not the whole room — a route offer goes to the one +/// machine it's for. +pub trait MeshClient: Send + Sync { + /// This phone's mesh device id (its ed25519-derived public id). + fn device_id(&self) -> String; + + /// Publish/refresh this phone's presence (its [`NodeProfile`]) to the + /// network. Called on join and whenever the profile changes. + fn advertise(&self, profile: &NodeProfile) -> MeshResult<()>; + + /// The device ids of peers the engine currently sees on this network. + fn peers(&self) -> Vec; + + /// Send a control message to one peer on [`CHANNEL_CONTROL`]. + fn send_control(&self, peer: &str, msg: &ControlMessage) -> MeshResult<()>; + + /// Send a pre-serialized media frame to one peer on [`CHANNEL_MEDIA`]. + /// Take a [`serde_json::Value`] so any of the plane frame types + /// (`InputEvent`, `TermFrame`, `FileFrame`, …) can be sent uniformly. + fn send_media(&self, peer: &str, payload: &serde_json::Value) -> MeshResult<()>; + + /// Serialize a typed media frame and send it. Provided over + /// [`MeshClient::send_media`] so callers hand it a `TermFrame` / + /// `InputEvent` / `FileFrame` directly. + fn send_frame(&self, peer: &str, frame: &T) -> MeshResult<()> { + let value = serde_json::to_value(frame)?; + self.send_media(peer, &value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::connect::{offer_terminal, route_id}; + use crate::media::TermPlane; + use allmystuff_graph::NodeId; + use allmystuff_protocol::RouteControl; + use std::sync::Mutex; + + #[derive(Default)] + struct FakeMesh { + id: String, + sent_control: Mutex>, + sent_media: Mutex>, + advertised: Mutex>, + peers: Vec, + } + + impl MeshClient for FakeMesh { + fn device_id(&self) -> String { + self.id.clone() + } + fn advertise(&self, profile: &NodeProfile) -> MeshResult<()> { + self.advertised.lock().unwrap().push(profile.clone()); + Ok(()) + } + fn peers(&self) -> Vec { + self.peers.clone() + } + fn send_control(&self, peer: &str, msg: &ControlMessage) -> MeshResult<()> { + self.sent_control + .lock() + .unwrap() + .push((peer.to_string(), msg.clone())); + Ok(()) + } + fn send_media(&self, peer: &str, payload: &serde_json::Value) -> MeshResult<()> { + self.sent_media + .lock() + .unwrap() + .push((peer.to_string(), payload.clone())); + Ok(()) + } + } + + #[test] + fn classify_routes_each_channel_to_its_type() { + // A control Accept on the control channel. + let accept = serde_json::json!({ + "t": "route", "kind": "accept", "route_id": "route:x→y", "session": "term-2" + }); + match classify(CHANNEL_CONTROL, "desk", accept) { + Some(Inbound::Control { + from, + msg: ControlMessage::Route(RouteControl::Accept { route_id, session }), + }) => { + assert_eq!(from, "desk"); + assert_eq!(route_id, "route:x→y"); + assert_eq!(session.as_deref(), Some("term-2")); + } + other => panic!("expected a route accept, got {other:?}"), + } + + // A terminal data frame on the media channel. + let term = serde_json::json!({ + "t": "term", "route": "route:r", "seq": 0, "kind": "data", "bytes": "aGk=" + }); + assert!(matches!( + classify(CHANNEL_MEDIA, "desk", term), + Some(Inbound::Media { + payload: MediaPayload::Terminal(_), + .. + }) + )); + + // An unknown channel is dropped. + assert!(classify("some/other/channel", "desk", serde_json::json!({})).is_none()); + } + + #[test] + fn end_to_end_open_a_terminal_against_the_fake_mesh() { + let phone = NodeId::from("phone"); + let desk = NodeId::from("desk"); + let mesh = FakeMesh { + id: "phone".into(), + peers: vec!["desk".into()], + ..Default::default() + }; + + // Offer a terminal to the desk. + let offer = offer_terminal(&desk, &phone, None, "tab-1"); + mesh.send_control("desk", &offer).unwrap(); + let rid = route_id("desk:terminal", "phone:term-view:tab-1"); + + // The desk accepts (its reply arrives off the control channel). + let accept = classify( + CHANNEL_CONTROL, + "desk", + serde_json::json!({ "t": "route", "kind": "accept", "route_id": rid, "session": "term-7" }), + ); + assert!(matches!( + accept, + Some(Inbound::Control { + msg: ControlMessage::Route(RouteControl::Accept { .. }), + .. + }) + )); + + // Type a command; it goes out on the media channel as a term frame. + let mut term = TermPlane::new(&rid); + mesh.send_frame("desk", &term.send(b"ls\n".to_vec())) + .unwrap(); + + let media = mesh.sent_media.lock().unwrap(); + assert_eq!(media.len(), 1); + assert_eq!(media[0].0, "desk"); + assert_eq!(media[0].1["t"], "term"); + assert_eq!(media[0].1["kind"], "data"); + + let ctl = mesh.sent_control.lock().unwrap(); + assert_eq!(ctl.len(), 1); + assert!(matches!( + ctl[0].1, + ControlMessage::Route(RouteControl::Offer { .. }) + )); + } +} diff --git a/docs/MOBILE.md b/docs/MOBILE.md new file mode 100644 index 00000000..7f80b8c4 --- /dev/null +++ b/docs/MOBILE.md @@ -0,0 +1,451 @@ +# AllMyStuff on iPhone & Android + +This document is the architecture and roadmap for the official AllMyStuff +mobile app. It is meant to be exhaustive: the crux that makes it possible, the +one architectural inversion the desktop never needed, exactly what code we +reuse, the wire contract the phone must speak byte-for-byte, the phased plan to +ship it, the risks, and the handful of decisions that are the product owner's +to make. + +Two slices of it already live in the repo: + +- the pure, transport-agnostic **core** of the mobile client, + [`crates/allmystuff-mobile-core`](../crates/allmystuff-mobile-core), tested + the same way every other AllMyStuff crate is (`cargo test -p + allmystuff-mobile-core`); and +- a runnable **Tauri mobile shell**, [`gui/mobile`](../gui/mobile), that wraps + the *exact* desktop Svelte UI (the graph/map, Console, Files, Terminal) and + answers what it can honestly from the core today — chiefly `scan_self`, which + puts a real phone node with the real viewer/controller capability set on the + graph. It type-checks for `aarch64-linux-android` in CI (`gui-mobile` job), + with no Android NDK, because it links no C-building crate yet. See §11 for + the one-command build. + +Everything in §3 marked "✅ in repo" is real today. + +--- + +## 0. What we're building + +The same thing AllMyStuff already is — *remote desktop, shell, and files for +every computer you own, over a private mesh* — reachable from your phone. You +open the app, your fleet is on the graph, you tap a machine and: + +- **watch and control its screen**, +- **open a real shell** on it, +- **browse and move its files**, + +with no VPN, no port forwarding, and **no central server** — the phone is a +true peer on your MyOwnMesh network, end-to-end encrypted to your machines. + +The phone is a **viewer and controller**, not a host: it consumes a remote +screen/camera/audio and reaches out with touch, keys, and file ops. It does not +host its own screen, a PTY, or input injection (those are desktop capture +concerns with no clean — or, for input injection, *any* — mobile story). +Phone-as-a-source (its camera/mic/screen shared *to* the fleet) is a later, +explicitly opt-in capability; see §8. + +--- + +## 1. The crux: can a phone join the mesh? + +**Yes — as a first-class peer, embedding the engine in-process.** This is the +whole reason the project is viable, so it's worth being precise. + +MyOwnMesh is **pure-Rust WebRTC**: the `webrtc` crate (v0.13 — ICE/DTLS/SCTP/ +SRTP/data), `ring` 0.17 for crypto, ed25519 device identity, and Nostr-over- +WebSocket for signaling. None of that is desktop-locked, and `myownmesh-core` +is explicitly **an embeddable library** ("pure Rust, embed it in anything"), +not just a daemon. Its facade is small and exactly what a client needs: + +``` +Mesh::open(MeshConfig) -> MeshHandle +MeshHandle::join(NetworkConfig) -> JoinedNetwork +JoinedNetwork::{ channel::(name), rpc(), peers(), roster_*(), advertise(caps), events() } +``` + +### The cross-compile verdict (Risk R1) + +The only open question was whether that stack cross-compiles to mobile +targets. We probed it: + +``` +$ rustup target add aarch64-linux-android +$ cargo check -p myownmesh-core --target aarch64-linux-android +``` + +**Result: the entire Rust dependency tree type-checks for +`aarch64-linux-android`** — `ring`, `tokio`, `nix`, `socket2`, `mio`, +`parking_lot`, `dashmap`, `ed25519-dalek`, `p256`, `aes-gcm`, and the rest of +the webrtc-rs stack. The build stopped at exactly one place: `ring`'s build +script couldn't find `aarch64-linux-android-clang` — **the Android NDK isn't +installed in the probe environment.** That is a build-infrastructure gap, *not* +a Rust portability blocker. `ring` 0.17 and `webrtc-rs` both support +iOS/Android upstream. + +**So the crux holds.** The remaining work is plumbing, not research: + +1. **Build infra** — the Android NDK + `cargo-ndk` (sets `CC_aarch64-linux-android`, + `AR`, the linker, and the sysroot); Xcode for the iOS targets + (`aarch64-apple-ios`, `aarch64-apple-ios-sim`). iOS cross-compilation needs + macOS and cannot be done from the Linux CI box that built the desktop. +2. **An FFI layer** over `myownmesh-core` (§5) — there is no `extern "C"`, + `#[no_mangle]`, `uniffi`, or `cdylib` in any MyOwnMesh crate today. +3. **Secret storage** — `identity.json` (a 0600 file) moves to the iOS Keychain + / Android Keystore. The mesh's `identity` loader assumes a filesystem path, + so it needs a storage seam. + +### The options we did *not* take + +| Option | Verdict | +|---|---| +| **(A) Embed `myownmesh-core` in-process** | **Chosen.** The phone is a real peer: own identity, direct DTLS/SRTP to peers, signaling only. Fully preserves the no-server/e2e promise. | +| (B) Relay through a trusted node you own | Fallback only. Still e2e *to the gateway*, but the phone isn't a peer — media decrypts and re-encrypts on a second hop, and you must run an always-on box. Documented escape hatch if R1's NDK/iOS plumbing ever proves intractable; never the headline. | +| (C) Re-implement the protocol on a native WebRTC SDK (libwebrtc) | Rejected. Re-derives the ed25519 handshake, Nostr room derivation, and `MeshMessage` framing from scratch — huge surface, constant drift, zero leverage from the existing crates. | +| (D) Cloud bridge (hosted relay) | **Rejected outright.** A central server that can see your media. It breaks the one promise the product is built on. We do not build this under any framing. | + +--- + +## 2. The one architectural inversion + +On the desktop, the GUI is a **thin client of a process it spawns**: + +``` +GUI ──(owner-only local socket)──► allmystuff-serve ──spawns──► myownmesh serve +(webview) (the node engine) (the mesh daemon) +``` + +`gui/src-tauri` connects to `~/.myownmesh/allmystuff-node.sock` (0600); the +node owns the `Mesh` and spawns the `myownmesh` daemon as a child process. The +GUI never speaks the mesh protocol directly — it issues `connect_route`, +`term_send`, … over that local socket and the node does the mesh work. + +**iOS forbids a sandboxed app from spawning child processes.** So that model +can't cross over. The mobile app inverts it: + +``` +Mobile app (one process) +├─ Svelte UI ──(Tauri commands / UniFFI)──► embedded myownmesh-core ──► the mesh +└─ native H.264/Opus decoders (VideoToolbox / MediaCodec) +``` + +The phone **is** the node. It speaks the **peer-to-peer** protocol — +`allmystuff-protocol` control messages and `allmystuff-session` media frames — +over MyOwnMesh's typed channels directly, because there is no local node to +delegate to. This is the opposite of the desktop's "always a control-socket +client" rule, and it is the single fact that shapes everything below. + +--- + +## 3. Framework & code reuse + +**Decision: Tauri 2 Mobile, reusing the Svelte 5 UI and the pure Rust crates. +Video decoded natively, not via WebCodecs.** + +The desktop is already Tauri **2.11** + Svelte **5** + Vite. Tauri 2 has +first-class `tauri ios` / `tauri android` targets — the mobile target has +simply never been initialized (no `gen/apple`, no `gen/android`). The Svelte +SPA already runs in a plain webview behind `isTauri()` guards. xterm.js (the +terminal) runs unchanged in a mobile webview. Choosing React Native or Flutter +would throw away the entire Svelte investment *and* still require the native +decode bridge — they buy nothing the Tauri + UniFFI combination doesn't. The +only credible alternative — fully native SwiftUI/Compose shells over the same +Rust core — stays in reserve for if WKWebView canvas performance for the video +stage proves inadequate (Risk R6). + +**The one webview concern is pre-solved.** `VideoDecoder` (WebCodecs) is +unreliable in WKWebView / Android System WebView. We sidestep it the way the +desktop already does for WebKitGTK: decode H.264 **natively** (VideoToolbox on +iOS, MediaCodec on Android) from the compressed access units that already cross +the mesh, and hand the webview ready-to-paint RGBA. We never pull raw RGBA over +the network (Risk R6) and never depend on the webview's own codec. + +### Reuse map + +| Crate / asset | On mobile | Notes | +|---|---|---| +| `allmystuff-graph` | ✅ reuse whole | `Catalog`, `Capability`, `Route`, `MediaKind`, `Flow`, `propose_route`, `match_endpoint`, the own-vs-share authorization model. Pure serde, `unsafe = forbid`. | +| `allmystuff-protocol` | ✅ reuse whole | `NodeProfile`, `ControlMessage`, `RouteControl`, channel/feature constants, `OwnedRoster`, `RoomMessage`. The wire vocabulary. | +| `allmystuff-session` | ✅ reuse whole | The media frame types (`VideoFrame`, `VideoAssembler`, `InputAction`, `TermEvent`, `FileEvent`, `AudioFrame`, `MediaPayload`) and the route state machine. The consumer-half spec. | +| `myownmesh-core` | ✅ embed (cross-compiled) | The mesh, in-process. The reason the plan works. | +| `allmystuff-mobile-core` | ✅ **in repo** (new) | The phone's capability model, the media decode/encode planes, route-offer helpers, and the `MeshClient` seam. Pure; tested. (§4) | +| `gui/mobile` (Tauri shell) | ✅ **in repo** (new) | The mobile Tauri 2 app: reuses the desktop `gui/src/**` Svelte UI verbatim (`frontendDist: "../dist"`), backed by `allmystuff-mobile-core`. Its own crate, so the desktop build is untouched; `tauri android/ios init` runs against it. (§11) | +| `allmystuff-bridge` | ↪ partial | Reuse the synthetic-endpoint *scheme*, not the hardware scan. The phone's capability builder is `allmystuff-mobile-core::caps` instead. | +| Svelte components | ↪ rework for touch | `Terminal.svelte` (xterm.js) ~free; `Graph.svelte`/`Files.svelte` need responsive + touch; `Console.svelte` renders fine but `input-keys.ts` is mouse/keyboard-only and must be rebuilt for touch. `App.svelte`'s multi-window routing becomes in-app navigation. | +| `node/` engine | ✖ excluded | `xcap`/`cpal`/`nokhwa`/`enigo`/`portable-pty` etc. are desktop *capture/injection*. A phone is a viewer, not a host. | +| `allmystuff-updater` | ✖ excluded (`cfg`) | Binary self-swap is forbidden on iOS / restricted on Android; the stores own updates. | +| Desktop Tauri shell | ✖ separate crate | The mobile shell is `gui/mobile`, *not* a `cfg`-gated `gui/src-tauri` — that keeps the desktop's tray / autostart / single-instance / OS-service / `externalBin` sidecars / `open_secondary_window` out of the mobile binary with zero risk to the green desktop build. The two share only the frontend. | + +--- + +## 4. What's in the repo today: `allmystuff-mobile-core` + +The transport-agnostic brain of the mobile client, pure Rust, fully unit +tested. It is written entirely against one seam — `MeshClient` — so every path +is exercised without a radio (an in-memory fake stands in for the embedded +engine in tests). + +``` +crates/allmystuff-mobile-core/src/ +├── lib.rs crate docs + prelude +├── caps.rs the phone's Capability set (viewer/controller; opt-in host) + advertised features +├── node.rs assemble the phone's NodeProfile for presence +├── connect.rs build the RouteControl::Offer for screen / camera / audio / terminal / files +├── transport.rs the MeshClient seam + classify(channel,from,payload) -> typed Inbound +└── media/ + ├── video.rs VideoSink (MJPEG reassembly) + VideoDecoder seam + RgbaFrame/H264Au/JpegFrame + ├── input.rs touch/keys -> normalized InputAction/InputEvent (clamped, per-route seq) + ├── term.rs keystrokes/resize up, PTY bytes down + └── files.rs request/reply FileClient: req-id allocation + Read chunk reassembly +``` + +Two correctness facts this crate pins down, both verified by tests and matched +to the existing desktop/`amst` behaviour: + +- **A phone needs a *display* sink, not just a video sink.** The graph routes a + remote *screen* (`MediaKind::Display`) only to a display sink and a remote + *camera* (`MediaKind::Video`) only to a video sink — exactly as the desktop + lands a remote screen on its physical monitor but a camera on its synthetic + `video-in`. A phone has no monitor to expose, so it advertises a synthetic + `display-in` (Display sink) that means "render a remote desktop here," plus + `video-in` for cameras. (`caps.rs`) +- **Terminal and files are synthetic `generic` routes.** They don't match + advertised capabilities; the host recognizes `:terminal` / + `:files` by id and authorizes them by *fleet membership*, not a grant. + The offer is byte-identical to what `amst` and the GUI send, down to the + route id `route:{from}→{to}`. (`connect.rs`) + +--- + +## 5. The FFI layer (`myownmesh-ffi`, to build) + +The embedded engine is reached through a thin FFI crate, added on the +**MyOwnMesh** side (the bindings belong with the dependency, and benefit any +future embedder). Recommended shape: + +- **Crate:** `crates/myownmesh-ffi`, `crate-type = ["lib", "staticlib", "cdylib"]`, + using **UniFFI** (proc-macro mode + `uniffi::setup_scaffolding!()`), with + `aarch64-apple-ios` / `aarch64-linux-android` added to MyOwnMesh CI. +- **A JSON boundary, not a typed one.** AllMyStuff already serializes + everything; the FFI surface should cross `String`/`bytes`, not the engine's + rich types (`MeshEvent`, `PeerInfo`, `IceCandidateStats`, …). This keeps the + binding tiny and avoids deriving UniFFI traits across the whole engine. +- **The surface** (one `MeshBridge` object; async via `#[uniffi::export(async_runtime = "tokio")]`): + + ```text + MeshBridge.open(identity_seed: bytes?) -> MeshBridge // load/generate identity (Keychain/Keystore-backed) + MeshBridge.device_id() -> String + MeshBridge.join(network_id: String, label: String) -> Result<()> + MeshBridge.advertise(profile_json: String) -> Result<()> // the NodeProfile from allmystuff-mobile-core + MeshBridge.peers() -> [String] + MeshBridge.send(peer: String, channel: String, payload_json: String) -> Result<()> + MeshBridge.roster_approve(device_id: String, label: String) -> Result<()> // pairing + // inbound via a foreign callback interface: + trait InboundSink { fn on_message(channel: String, from: String, payload_json: String); fn on_event(event_json: String); } + ``` + + The Rust side maps `send`/`on_message` onto `JoinedNetwork::channel::(name)` + (monomorphizing the generic `Channel` to a JSON channel), and fans + `events()` into `on_event`. The mobile app then drives it entirely through + `allmystuff-mobile-core`: build the `NodeProfile`/offers there, hand JSON to + `send`, and feed `on_message`'s `(channel, from, payload)` straight into + `allmystuff_mobile_core::transport::classify`. +- **Secret storage seam:** `open(identity_seed)` lets the platform supply the + ed25519 seed from Keychain/Keystore instead of the engine reading a file. + +This crate is the right size to build and verify on host first (UniFFI compiles +on host), then wire into the cross-compile once the NDK/Xcode infra lands. + +--- + +## 6. The wire contract the phone must honour + +Everything below is what `allmystuff-mobile-core` already encodes; it's +restated here as the implementer's checklist. **All of it is JSON + base64 over +the mesh's typed channels, plus two RTP track lanes.** Forward-compatibility is +mandatory throughout: an unknown tag/kind decodes to `Unknown` (or `None`) and +is ignored — never error, or a peer vanishes. + +**Channels** (`allmystuff-protocol`): `allmystuff/presence/v1`, +`/control/v1`, `/media/v1`, `/owned/v1`, `/rooms/v1`. + +**Decode (inbound, the viewer half):** + +| Plane | Transport | Decode | +|---|---|---| +| Screen / camera (H.264) | RTP lane (`video_inbound`) | Annex-B access units → VideoToolbox/MediaCodec. `key` = IDR entry point; PTS = `rtp_ts * 1000 / 90`. | +| Screen / camera (MJPEG fallback) | `t:"video"` on media channel | standalone baseline JPEGs, chunked across ~64 KiB messages sharing a `seq`; reassemble with `VideoAssembler` (done in `media::VideoSink`). | +| Capture status | `t:"vstat"` | render `VideoStatusState` instead of a black stage. | +| Audio (Opus) | RTP lane (`audio_inbound`) | 48 kHz. | +| Audio (PCM fallback) | untagged frame on media channel | interleaved S16LE, base64. | +| Terminal | `t:"term"` (`TermEvent::Data`) | raw VT bytes → xterm.js. | + +**Encode (outbound, the controller half) — all plain JSON, no codec:** + +| Plane | Frame | Built by | +|---|---|---| +| Input | `InputAction` (mouse_move normalized 0..1, mouse_button, wheel, key as DOM `key`/`code`) | `media::InputEncoder` | +| Terminal | `TermEvent::Data` / `Resize` | `media::TermPlane` | +| Files | `FileEvent` (list/read/write/mkdir/rename/delete, viewer-minted `req`) | `media::FileClient` | +| Route setup | `ControlMessage::Route(RouteControl::Offer{route,video,audio,session})` | `connect::offer_*` | + +**Route ids** are `route:{from}→{to}` (note the literal `→`), derived +identically on both ends. **base64 is always the STANDARD engine.** + +--- + +## 7. Connection & pairing + +1. **Identity** — load or generate the phone's ed25519 identity into the + Keychain/Keystore; this is its mesh device id. +2. **Join** — `Mesh::open` → `MeshHandle::join(network)`. Signaling is Nostr + over WebSocket, reachable on cellular/Wi-Fi; STUN/TURN likewise. +3. **Pair into the fleet** — the phone joins via the existing claim/ownership + model (`OwnedRoster`). A desktop owner approves it; the bilateral + verification (the 5-char device suffix + the verification code each side + reads back) is the out-of-band confirm. **No new auth mechanism** — this is + the same device-trust primitive the desktop uses. Fleet membership is what + gates terminal/files/input (owner-or-fleet), distinct from share-grants + (which gate media from peers you don't own). +4. **Advertise** — publish the phone's `NodeProfile` (from + `allmystuff-mobile-core::node::mobile_profile`) on presence. + +**Background limits:** iOS aggressively suspends a persistent WebRTC node in +the background. v1–v3 treat the mesh node as **foreground-only** (live while the +app is open). Persistent background presence (push-wake, etc.) is a separate +effort and may never fully match the desktop; set expectations accordingly. + +--- + +## 8. Phased plan + +Sequenced by **codec dependency and risk** — the lightest plane first, the +hardest capture last. + +**Phase 0 — De-risk & scaffold.** +- ✅ Cross-compile probe (R1): pure-Rust tree checks for Android; only the NDK is missing. +- ✅ `allmystuff-mobile-core` (this PR): the tested, transport-agnostic core. +- ⬜ Stand up the Android NDK + `cargo-ndk` and an iOS build host; cross-compile + `myownmesh-core` clean on both (R1 finish). +- ⬜ `myownmesh-ffi` (§5); prove `open → join → exchange one message` with a desktop peer. + +**Phase 1 — v1: Graph + Terminal (smallest shippable).** +- ✅ Mobile entry point + Tauri shell as its own crate (`gui/mobile`), reusing + the desktop Svelte UI; `scan_self` backed by `allmystuff-mobile-core`; + android-target CI check. `tauri android/ios init` runs against it (§11). +- ⬜ Embed `myownmesh-core` via the FFI; identity in Keychain/Keystore; fleet pairing. +- ⬜ Render `Graph.svelte` (responsive) for discovery; reuse the graph/protocol/session crates whole. +- **Terminal**: `offer_terminal` → `TermPlane` ↔ xterm.js, plus an on-screen + modifier bar (Ctrl/Esc/arrows/Tab). Pure JSON, no codec, no decoder — that's + why it's first. +- Ship to TestFlight / Play internal. **Deliverable: open a remote shell from your phone.** + +**Phase 2 — v2: Remote desktop (view) + Files.** +- **Native video decode bridge** (highest-risk media work): VideoToolbox/MediaCodec + fed Annex-B AUs; MJPEG fallback via `VideoSink`. Render to a `Console`-style canvas. +- **Touch input forwarding**: rebuild `input-keys.ts` for touch → `InputEncoder` + (tap→click, drag→move, pinch/scroll→wheel, soft keyboard→`Key`). Now the phone *controls*. +- `tune_route` / `video_feedback` / `Refresh` for quality adaptation. +- **Files**: `FileClient` + a touch file browser; downloads stream to phone storage. + +**Phase 3 — v3: Audio + Camera + Clipboard + Rooms (toward parity).** +- Opus + PCM decode (and Opus encode if the phone's mic becomes a source). +- **Camera-as-source** (opt-in host scope): phone camera → H.264 → offer a video + route. First time the phone *hosts* capture (AVFoundation/Camera2); last and optional. +- Clipboard sync; Rooms; background-presence investigation. + +--- + +## 9. The decisions that are the product owner's to make + +These shape the build and are hard to reverse. Defaults below are what the +code and this plan already assume; flag any you'd change. + +1. **Mesh path — embedded peer (A).** *Default: A.* The only path that keeps + "the phone is a true p2p peer, no central server." B (relay) is the + documented fallback if the iOS/NDK plumbing ever proves intractable. **D + (cloud bridge) is off the table** — it breaks the core promise. +2. **Scope — viewer/controller, host opt-in later.** *Default: viewer/controller + for v1–v2; camera-as-source opt-in in v3; never a screen/PTY/input-injection + host.* Determines whether the phone ever advertises *source* capabilities. + (`MobileScope` already encodes both.) +3. **UI — Tauri + Svelte.** *Default: reuse the Svelte UI.* Going native + (SwiftUI/Compose) only if WKWebView video performance is unacceptable after + the Phase 2 spike (R6). +4. **Background presence — foreground-only.** *Default: foreground-only v1–v3.* + Persistent presence fights the OS and burns battery; a separate effort. +5. **FFI home — the MyOwnMesh repo.** *Default: `myownmesh-ffi` lives with the + transport.* Same owner as AllMyStuff, so coordination is light, but it is a + second-repo change. + +--- + +## 10. Risks & the cheapest experiment for each + +| # | Risk | Cheapest experiment | Status | +|---|---|---|---| +| R1 | `myownmesh-core` cross-compiles to iOS/Android? | `cargo check --target` on both | ✅ Android Rust tree checks; only NDK missing. iOS needs a macOS host. | +| R2 | Can webrtc-rs's ICE enumerate candidates in the iOS sandbox / on cellular? Entitlements? | minimal iOS app, one peer on Wi-Fi then cellular | ⬜ run right after R1 finishes | +| R3 | App Store viability of an embedded full WebRTC stack | internal TestFlight build on a real signed device | ⬜ at first TestFlight | +| R4 | VideoToolbox/MediaCodec accept openh264's profile/level? | capture real `video_inbound` AUs, feed a standalone decoder harness on-device | ⬜ start of Phase 2 | +| R5 | MyOwnMesh owner accepts the FFI crate; idle media-lane battery cost | socialize the FFI plan (same owner); battery-measure an idle joined node | ⬜ before Phase 1 | +| R6 | WKWebView canvas throughput for the video stage | decode-on-device → canvas paint benchmark; native shell fallback if poor | ⬜ Phase 2 | + +--- + +## 11. Build & release deltas + +### Build the shell now + +The shell is in `gui/mobile`. It reuses the desktop frontend, so the only +extra inputs are the platform toolchains. From `gui/mobile`: + +```sh +# one-time: the mobile Rust targets + the Tauri CLI for this package +rustup target add aarch64-linux-android armv7-linux-androideabi \ + aarch64-apple-ios aarch64-apple-ios-sim +pnpm install # just the Tauri CLI; the UI installs in ../ on build + +# Android — needs the Android SDK + NDK on PATH (ANDROID_HOME, NDK_HOME) and +# cargo-ndk (`cargo install cargo-ndk`): +pnpm tauri android init # generates gen/android (git-ignored) +pnpm tauri android dev # run on a device/emulator +pnpm tauri android build # -> .apk / .aab + +# iOS — needs macOS + Xcode + an Apple Developer signing identity: +pnpm tauri ios init # generates gen/apple (git-ignored) +pnpm tauri ios dev # run in the simulator / on device +pnpm tauri ios build # -> .ipa +``` + +`tauri android/ios init` regenerates the native Gradle/Xcode projects under +`gen/` from the shell + config, so they're intentionally **not** committed. +CI's `gui-mobile` job proves the Rust backend keeps type-checking for Android +on every push (no NDK needed until the embedded engine lands). Desktop smoke +test of the same shell on a GTK box: `pnpm tauri dev`. + +### Release deltas + +Adding mobile means, beyond the desktop's existing Linux CI + minisign-signed +releases: + +- **Android**: NDK + `cargo-ndk`; build the `.aab`/`.apk`; sign with a Play + upload key; Play Console internal → closed → open tracks. +- **iOS**: a macOS build host with Xcode; `tauri ios build` → `.ipa`; an Apple + Developer account, signing certs + provisioning profiles; TestFlight → App + Review. (Cannot be produced on the Linux desktop-CI box.) +- **`cfg` gating**: `tauri.conf.json`'s `externalBin` sidecars, `build.rs` + sidecar bundling, and the desktop-shell Tauri commands are desktop-only and + must be excluded from the mobile target. The updater crate is excluded + entirely. + +--- + +*The bottom line: a Tauri Mobile app in this repo that embeds `myownmesh-core` +as a true mesh peer via a small UniFFI layer, reuses the pure +graph/protocol/session crates and the Svelte UI, decodes media natively, and +ships terminal-first → desktop-view → audio/camera. The crux is settled; the +core (`allmystuff-mobile-core`) and a runnable Tauri shell (`gui/mobile`, +android-checked in CI) are in the repo; the next gate is the embedded-engine +FFI crate (wiring the `MeshClient` seam to `myownmesh-core`) plus the +NDK/iOS cross-compile (R1's tail).* diff --git a/gui/mobile/.gitignore b/gui/mobile/.gitignore new file mode 100644 index 00000000..368d4b91 --- /dev/null +++ b/gui/mobile/.gitignore @@ -0,0 +1,10 @@ +# Rust build output +/target + +# Tauri-generated native projects (regenerated by `tauri android/ios init`). +# The shell, config, and Rust backend are the source of truth; the Xcode / +# Gradle projects are generated artifacts. +/gen + +# This package only carries the Tauri CLI; deps install on demand. +/node_modules diff --git a/gui/mobile/Cargo.lock b/gui/mobile/Cargo.lock new file mode 100644 index 00000000..4bf2bf3b --- /dev/null +++ b/gui/mobile/Cargo.lock @@ -0,0 +1,4652 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allmystuff-graph" +version = "0.2.4" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "allmystuff-mobile" +version = "0.2.4" +dependencies = [ + "allmystuff-mobile-core", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", +] + +[[package]] +name = "allmystuff-mobile-core" +version = "0.2.4" +dependencies = [ + "allmystuff-graph", + "allmystuff-protocol", + "allmystuff-session", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "allmystuff-protocol" +version = "0.2.4" +dependencies = [ + "allmystuff-graph", + "serde", + "serde_json", +] + +[[package]] +name = "allmystuff-session" +version = "0.2.4" +dependencies = [ + "allmystuff-graph", + "allmystuff-protocol", + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[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 = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[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.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[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" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[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-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[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 = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 2.0.118", +] + +[[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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[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-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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 2.0.118", +] + +[[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-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[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 = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +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", + "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 = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[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", + "tinystr", + "writeable", + "zerovec", +] + +[[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", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[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 = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[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 = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[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 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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 = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[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 = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "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.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[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", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[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.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[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 = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[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-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[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 = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +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 = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[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 = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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 = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[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 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[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 = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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 = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[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 2.0.118", +] + +[[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 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[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", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[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-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[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.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[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 2.14.0", + "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_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[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-core", +] + +[[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 = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[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 = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[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.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[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 = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[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 2.0.118", +] + +[[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 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[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 0.2.1", + "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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[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 = "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 2.0.118", + "synstructure", +] + +[[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 2.0.118", + "synstructure", +] + +[[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 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/gui/mobile/Cargo.toml b/gui/mobile/Cargo.toml new file mode 100644 index 00000000..73498645 --- /dev/null +++ b/gui/mobile/Cargo.toml @@ -0,0 +1,51 @@ +# The AllMyStuff **mobile** Tauri shell (iOS / Android). +# +# Like `../src-tauri`, this crate lives outside the main Cargo workspace — +# an empty `[workspace]` table makes it its own single-member root — so a +# `cargo build --workspace` at the repo root stays webview-free. It reuses +# the *exact* Svelte frontend the desktop ships (see `tauri.conf.json`'s +# `frontendDist = "../dist"`); the only thing that differs is this Rust +# backend, which embeds the phone's viewer/controller brain +# (`allmystuff-mobile-core`) instead of spawning a desktop node over a +# control socket — iOS forbids the child process the desktop model needs. +[workspace] + +[package] +name = "allmystuff-mobile" +version = "0.2.4" +description = "AllMyStuff mobile (iOS/Android) — a Tauri 2 shell around the shared Svelte UI, backed by the phone's embedded viewer/controller core." +authors = ["Chris Paul"] +license = "MIT" +edition = "2021" +rust-version = "1.88.0" + +# Tauri's mobile targets link the app as a library (the platform shell — an +# Android `NativeActivity` / an iOS view controller — calls `run()` through +# the C ABI), while the desktop smoke-test build is an ordinary binary. Both +# entry points funnel into `run()` in `lib.rs`. +[lib] +name = "allmystuff_mobile_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "allmystuff-mobile" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +# No `tray-icon` / `autostart` / `single-instance` here — those are desktop +# concerns with no mobile story. `shell` stays for opening external links. +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" + +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# The phone's transport-agnostic brain: its capability model and the +# client-side media planes. Pure Rust, no native deps — it gives us a real +# phone `NodeProfile` (identity + viewer/controller capabilities) to put on +# the graph today, and the `MeshClient` seam the embedded engine fills in +# next. Pulled by path; it inherits its versions from the main workspace. +allmystuff-mobile-core = { path = "../../crates/allmystuff-mobile-core" } diff --git a/gui/mobile/build.rs b/gui/mobile/build.rs new file mode 100644 index 00000000..261851f6 --- /dev/null +++ b/gui/mobile/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/gui/mobile/capabilities/default.json b/gui/mobile/capabilities/default.json new file mode 100644 index 00000000..0477e397 --- /dev/null +++ b/gui/mobile/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "default", + "description": "Core webview permissions plus opening external links in the system browser.", + "windows": ["main"], + "permissions": ["core:default", "shell:allow-open"] +} diff --git a/gui/mobile/package.json b/gui/mobile/package.json new file mode 100644 index 00000000..0df056c4 --- /dev/null +++ b/gui/mobile/package.json @@ -0,0 +1,13 @@ +{ + "name": "allmystuff-mobile", + "private": true, + "version": "0.2.4", + "type": "module", + "description": "AllMyStuff mobile (iOS/Android) Tauri shell. Reuses the desktop GUI's Svelte frontend (../) verbatim; this package only carries the Tauri CLI and the mobile Rust backend.", + "scripts": { + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} diff --git a/gui/mobile/src/lib.rs b/gui/mobile/src/lib.rs new file mode 100644 index 00000000..f5376ce6 --- /dev/null +++ b/gui/mobile/src/lib.rs @@ -0,0 +1,100 @@ +//! AllMyStuff mobile (iOS / Android) — Tauri shell. +//! +//! The window is the **same** Svelte app the desktop ships (the graph/map, +//! Console, Files, Terminal, Rooms — see `../../gui/src`). The frontend is +//! built to run with or without a backend: every call goes through a +//! `tryInvoke` that falls back to demo data when a command is absent, so the +//! UI is fully navigable from the first build while the native planes land +//! one at a time. +//! +//! What differs from the desktop shell (`../src-tauri/src/main.rs`) is *how* +//! this Rust side answers. The desktop spawns a separate `allmystuff-serve` +//! node and drives it over a control socket. iOS forbids a sandboxed app +//! from spawning child processes, so the phone instead **embeds** its +//! viewer/controller brain in-process: this shell is built on +//! [`allmystuff_mobile_core`], and the live mesh engine drops in behind that +//! crate's `MeshClient` seam (the embedded `myownmesh-core` binding — the +//! next slice; see `docs/MOBILE.md`). +//! +//! Today this shell answers exactly the calls it can answer *honestly* from +//! `mobile-core` — chiefly [`scan_self`], which puts a real phone node on the +//! graph with the real viewer/controller capability set. Calls that need a +//! live mesh (presence, routing, the media planes) are intentionally not +//! registered yet, so the frontend keeps its demo behaviour for them until +//! the engine binding makes them real, rather than this side faking a mesh. + +use allmystuff_mobile_core::prelude::*; +use serde_json::{json, Value}; + +/// A friendly host-OS string for the node card — "Android", "iOS", or the +/// raw target name on a desktop smoke-test build. +fn os_label() -> &'static str { + match std::env::consts::OS { + "android" => "Android", + "ios" => "iOS", + other => other, + } +} + +/// Seconds since the Unix epoch — the phone's per-run boot id. A peer that +/// sees a new value knows the phone restarted, the same event-driven gossip +/// the desktop uses. +fn boot_id() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// Scan "this machine" — the phone. Mirrors the desktop `scan_self`'s +/// `ScanResult` shape (`{ node_id, label, hostname, summary, capabilities }`) +/// so the store re-homes the local node with no mobile special case. +/// +/// `node_id` is `"this"` until the embedded engine assigns the phone its real +/// ed25519 device id — exactly the placeholder the desktop uses for its +/// offline/demo graph, which the store already understands. The capabilities, +/// though, are real: the viewer/controller set every phone advertises, built +/// by [`mobile_capabilities`] the same way a desktop's come from its hardware +/// scan. +#[tauri::command] +fn scan_self() -> Result { + // "this" mirrors the desktop's offline-graph placeholder; the engine + // swaps in the real device id once it's up. + let node = NodeId::from("this"); + let cfg = MobileNodeConfig { + label: "My Phone".to_string(), + os: os_label().to_string(), + model: String::new(), + ram_bytes: 0, + scope: MobileScope::ViewerController, + }; + let profile = mobile_profile(&node, &cfg, boot_id(), env!("CARGO_PKG_VERSION")); + + Ok(json!({ + "node_id": profile.node.as_str(), + "label": profile.label, + "hostname": "", + "summary": profile.summary, + "capabilities": profile.capabilities, + })) +} + +/// Mirror one frontend diagnostic line into the app's stderr log, so a +/// `adb logcat` / Xcode console reads a call end to end — the mobile +/// counterpart to the desktop's `client_log`. +#[tauri::command] +fn client_log(line: String) { + eprintln!("[ui] {line}"); +} + +/// The shared entry point. On iOS/Android the platform shell calls this +/// through the C ABI (`tauri::mobile_entry_point`); on a desktop smoke-test +/// build `main.rs` calls it directly. +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .invoke_handler(tauri::generate_handler![scan_self, client_log]) + .run(tauri::generate_context!()) + .expect("error while running the AllMyStuff mobile app"); +} diff --git a/gui/mobile/src/main.rs b/gui/mobile/src/main.rs new file mode 100644 index 00000000..10c962e1 --- /dev/null +++ b/gui/mobile/src/main.rs @@ -0,0 +1,14 @@ +//! Desktop smoke-test entry point. +//! +//! On iOS/Android the platform shell enters through +//! `allmystuff_mobile_lib::run` (the `tauri::mobile_entry_point`); this `main` +//! exists so `pnpm tauri dev` can bring the same shell up in a desktop window +//! to iterate on the UI without a device or emulator. +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +fn main() { + allmystuff_mobile_lib::run(); +} diff --git a/gui/mobile/tauri.conf.json b/gui/mobile/tauri.conf.json new file mode 100644 index 00000000..999cb159 --- /dev/null +++ b/gui/mobile/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "AllMyStuff", + "version": "0.2.4", + "identifier": "works.allmystuff.mobile", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1430", + "beforeDevCommand": "pnpm --dir .. dev", + "beforeBuildCommand": "pnpm --dir .. build" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "AllMyStuff" + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../src-tauri/icons/32x32.png", + "../src-tauri/icons/128x128.png", + "../src-tauri/icons/128x128@2x.png", + "../src-tauri/icons/icon.png", + "../src-tauri/icons/icon.icns", + "../src-tauri/icons/icon.ico" + ] + } +}